From 939d2ac58b1ed2f3303d35f79671dfd5d1b061cf Mon Sep 17 00:00:00 2001 From: daxpedda Date: Fri, 27 Dec 2019 11:32:02 +0100 Subject: [PATCH 01/11] Convert `console`. --- src/services/console.rs | 54 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/src/services/console.rs b/src/services/console.rs index 47d441a9df4..1fcb425b58e 100644 --- a/src/services/console.rs +++ b/src/services/console.rs @@ -1,7 +1,10 @@ //! This module contains a service implementation to use browser's console. +#[cfg(feature = "stdweb")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; +#[cfg(feature = "web_sys")] +use web_sys::console; /// A service to use methods of a /// [Console](https://developer.mozilla.org/en-US/docs/Web/API/Console). @@ -17,101 +20,152 @@ impl ConsoleService { /// [console.log](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) /// method implementation. pub fn log(&mut self, message: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.log(@{message}); } + #[cfg(feature = "web_sys")] + console::log_1(message.into()); } /// [console.warn](https://developer.mozilla.org/en-US/docs/Web/API/Console/warn) /// method implementation. pub fn warn(&mut self, message: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.warn(@{message}); } + #[cfg(feature = "web_sys")] + console::warn_1(message.into()); } /// [console.info](https://developer.mozilla.org/en-US/docs/Web/API/Console/info) /// method implementation. pub fn info(&mut self, message: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.info(@{message}); } + #[cfg(feature = "web_sys")] + console::info_1(message.into()); } /// [console.error](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) /// method implementation. pub fn error(&mut self, message: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.error(@{message}); } + #[cfg(feature = "web_sys")] + console::error_1(message.into()); } /// [console.debug](https://developer.mozilla.org/en-US/docs/Web/API/Console/debug) /// method implementation. pub fn debug(&mut self, message: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.debug(@{message}); } + #[cfg(feature = "web_sys")] + console::debug_1(message.into()); } /// [console.count_named](https://developer.mozilla.org/en-US/docs/Web/API/Console/count_named) /// method implementation. pub fn count_named(&mut self, name: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.count(@{name}); } + #[cfg(feature = "web_sys")] + console::count_with_label(name.into()); } /// [console.count](https://developer.mozilla.org/en-US/docs/Web/API/Console/count) /// method implementation. pub fn count(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.count(); } + #[cfg(feature = "web_sys")] + console::count(); } /// [console.time_named](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_named) /// method implementation. pub fn time_named(&mut self, name: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.time(@{name}); } + #[cfg(feature = "web_sys")] + console::time_with_label(name.into()); } /// [console.time_named_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_named_end) /// method implementation. pub fn time_named_end(&mut self, name: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.timeEnd(@{name}); } + #[cfg(feature = "web_sys")] + console::time_end_with_label(name.into()); } /// [console.time](https://developer.mozilla.org/en-US/docs/Web/API/Console/time) /// method implementation. pub fn time(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.time(); } + #[cfg(feature = "web_sys")] + console::time(); } /// [console.time_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_end) /// method implementation. pub fn time_end(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.timeEnd(); } + #[cfg(feature = "web_sys")] + console::time_end(); } /// [console.clear](https://developer.mozilla.org/en-US/docs/Web/API/Console/clear) /// method implementation. pub fn clear(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.clear(); } + #[cfg(feature = "web_sys")] + console::clear(); } /// [console.group](https://developer.mozilla.org/en-US/docs/Web/API/Console/group) /// method implementation. pub fn group(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.group(); } + #[cfg(feature = "web_sys")] + console::group_0(); } /// [console.group_collapsed](https://developer.mozilla.org/en-US/docs/Web/API/Console/group_collapsed) /// method implementation. pub fn group_collapsed(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.groupCollapsed(); } + #[cfg(feature = "web_sys")] + console::group_collapsed_0(); } /// [console.group_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/group_end) /// method implementation. pub fn group_end(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.groupEnd(); } + #[cfg(feature = "web_sys")] + console::group_end(); } /// [console.trace](https://developer.mozilla.org/en-US/docs/Web/API/Console/trace) /// method implementation. pub fn trace(&mut self) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.trace(); } + #[cfg(feature = "web_sys")] + console::trace_0(); } /// [console.assert](https://developer.mozilla.org/en-US/docs/Web/API/Console/assert) /// method implementation. pub fn assert(&mut self, condition: bool, message: &str) { + #[cfg(feature = "stdweb")] js! { @(no_return) console.assert(@{condition}, @{message}); } + #[cfg(feature = "web_sys")] + console::assert_with_condition_and_data_1(condition, message.into()); } } From 626cf9bbb534617908a10e2abc0ff1c3ef84e7eb Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 31 Dec 2019 15:36:17 +0100 Subject: [PATCH 02/11] Finish services. --- src/services/console.rs | 54 +++---- src/services/dialog.rs | 24 ++- src/services/fetch.rs | 331 +++++++++++++++++++++++++++++++++----- src/services/interval.rs | 65 ++++++-- src/services/keyboard.rs | 94 ++++++++--- src/services/reader.rs | 129 ++++++++++++--- src/services/render.rs | 44 ++++- src/services/resize.rs | 42 +++-- src/services/storage.rs | 33 ++-- src/services/timeout.rs | 15 +- src/services/websocket.rs | 125 +++++++++++--- 11 files changed, 785 insertions(+), 171 deletions(-) diff --git a/src/services/console.rs b/src/services/console.rs index 1fcb425b58e..9f9f73c725c 100644 --- a/src/services/console.rs +++ b/src/services/console.rs @@ -1,6 +1,6 @@ //! This module contains a service implementation to use browser's console. -#[cfg(feature = "stdweb")] +#[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; #[cfg(feature = "web_sys")] @@ -20,61 +20,61 @@ impl ConsoleService { /// [console.log](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) /// method implementation. pub fn log(&mut self, message: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.log(@{message}); } #[cfg(feature = "web_sys")] - console::log_1(message.into()); + console::log_1(&String::from(message).into()); } /// [console.warn](https://developer.mozilla.org/en-US/docs/Web/API/Console/warn) /// method implementation. pub fn warn(&mut self, message: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.warn(@{message}); } #[cfg(feature = "web_sys")] - console::warn_1(message.into()); + console::warn_1(&String::from(message).into()); } /// [console.info](https://developer.mozilla.org/en-US/docs/Web/API/Console/info) /// method implementation. pub fn info(&mut self, message: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.info(@{message}); } #[cfg(feature = "web_sys")] - console::info_1(message.into()); + console::info_1(&String::from(message).into()); } /// [console.error](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) /// method implementation. pub fn error(&mut self, message: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.error(@{message}); } #[cfg(feature = "web_sys")] - console::error_1(message.into()); + console::error_1(&String::from(message).into()); } /// [console.debug](https://developer.mozilla.org/en-US/docs/Web/API/Console/debug) /// method implementation. pub fn debug(&mut self, message: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.debug(@{message}); } #[cfg(feature = "web_sys")] - console::debug_1(message.into()); + console::debug_1(&String::from(message).into()); } /// [console.count_named](https://developer.mozilla.org/en-US/docs/Web/API/Console/count_named) /// method implementation. pub fn count_named(&mut self, name: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.count(@{name}); } #[cfg(feature = "web_sys")] - console::count_with_label(name.into()); + console::count_with_label(name); } /// [console.count](https://developer.mozilla.org/en-US/docs/Web/API/Console/count) /// method implementation. pub fn count(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.count(); } #[cfg(feature = "web_sys")] console::count(); @@ -83,25 +83,25 @@ impl ConsoleService { /// [console.time_named](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_named) /// method implementation. pub fn time_named(&mut self, name: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.time(@{name}); } #[cfg(feature = "web_sys")] - console::time_with_label(name.into()); + console::time_with_label(name); } /// [console.time_named_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_named_end) /// method implementation. pub fn time_named_end(&mut self, name: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.timeEnd(@{name}); } #[cfg(feature = "web_sys")] - console::time_end_with_label(name.into()); + console::time_end_with_label(name); } /// [console.time](https://developer.mozilla.org/en-US/docs/Web/API/Console/time) /// method implementation. pub fn time(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.time(); } #[cfg(feature = "web_sys")] console::time(); @@ -109,7 +109,7 @@ impl ConsoleService { /// [console.time_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_end) /// method implementation. pub fn time_end(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.timeEnd(); } #[cfg(feature = "web_sys")] console::time_end(); @@ -118,7 +118,7 @@ impl ConsoleService { /// [console.clear](https://developer.mozilla.org/en-US/docs/Web/API/Console/clear) /// method implementation. pub fn clear(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.clear(); } #[cfg(feature = "web_sys")] console::clear(); @@ -127,7 +127,7 @@ impl ConsoleService { /// [console.group](https://developer.mozilla.org/en-US/docs/Web/API/Console/group) /// method implementation. pub fn group(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.group(); } #[cfg(feature = "web_sys")] console::group_0(); @@ -136,7 +136,7 @@ impl ConsoleService { /// [console.group_collapsed](https://developer.mozilla.org/en-US/docs/Web/API/Console/group_collapsed) /// method implementation. pub fn group_collapsed(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.groupCollapsed(); } #[cfg(feature = "web_sys")] console::group_collapsed_0(); @@ -145,7 +145,7 @@ impl ConsoleService { /// [console.group_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/group_end) /// method implementation. pub fn group_end(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.groupEnd(); } #[cfg(feature = "web_sys")] console::group_end(); @@ -154,7 +154,7 @@ impl ConsoleService { /// [console.trace](https://developer.mozilla.org/en-US/docs/Web/API/Console/trace) /// method implementation. pub fn trace(&mut self) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.trace(); } #[cfg(feature = "web_sys")] console::trace_0(); @@ -163,9 +163,9 @@ impl ConsoleService { /// [console.assert](https://developer.mozilla.org/en-US/docs/Web/API/Console/assert) /// method implementation. pub fn assert(&mut self, condition: bool, message: &str) { - #[cfg(feature = "stdweb")] + #[cfg(feature = "std_web")] js! { @(no_return) console.assert(@{condition}, @{message}); } #[cfg(feature = "web_sys")] - console::assert_with_condition_and_data_1(condition, message.into()); + console::assert_with_condition_and_data_1(condition, &String::from(message).into()); } } diff --git a/src/services/dialog.rs b/src/services/dialog.rs index 8410d6ce7c1..da02db41ed6 100644 --- a/src/services/dialog.rs +++ b/src/services/dialog.rs @@ -1,7 +1,9 @@ //! This module contains the implementation of a service //! to show alerts and confirm dialogs in a browser. +#[cfg(feature = "std_web")] use stdweb::Value; +#[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; @@ -18,16 +20,30 @@ impl DialogService { /// Calls [alert](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) /// function. pub fn alert(&mut self, message: &str) { + #[cfg(feature = "std_web")] js! { @(no_return) alert(@{message}); } + #[cfg(feature = "web_sys")] + web_sys::window() + .unwrap() + .alert_with_message(message) + .unwrap(); } /// Calls [confirm](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) /// function. pub fn confirm(&mut self, message: &str) -> bool { - let value: Value = js! { return confirm(@{message}); }; - match value { - Value::Bool(result) => result, - _ => false, + #[cfg(feature = "std_web")] + { + let value: Value = js! { return confirm(@{message}); }; + match value { + Value::Bool(result) => result, + _ => false, + } } + #[cfg(feature = "web_sys")] + web_sys::window() + .unwrap() + .confirm_with_message(message) + .unwrap() } } diff --git a/src/services/fetch.rs b/src/services/fetch.rs index 49b8f2829a6..e7c7afcc807 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -4,19 +4,60 @@ use super::Task; use crate::callback::Callback; use crate::format::{Binary, Format, Text}; use failure::Fail; -use serde::Serialize; -use std::collections::HashMap; use std::fmt; -use stdweb::serde::Serde; -use stdweb::unstable::{TryFrom, TryInto}; -use stdweb::web::ArrayBuffer; -use stdweb::{JsSerialize, Value}; +#[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; +#[cfg(feature = "web_sys")] +use ::{ + js_sys::{Array, Uint8Array}, + std::{ + rc::Rc, + sync::{ + atomic::{AtomicBool, Ordering}, + mpsc::{self, Receiver}, + }, + }, + wasm_bindgen::{closure::Closure, JsValue}, + web_sys::{ + AbortController, Headers, Request as WebRequest, RequestCache as Cache, + RequestCredentials as Credentials, RequestInit, RequestMode as Mode, + RequestRedirect as Redirect, Response as WebResponse, + }, +}; +#[cfg(feature = "std_web")] +use ::{ + serde::Serialize, + std::collections::HashMap, + stdweb::{ + serde::Serde, + unstable::{TryFrom, TryInto}, + web::ArrayBuffer, + JsSerialize, Value, + }, +}; pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri}; +#[cfg(feature = "web_sys")] +struct ArrayBuffer(Uint8Array); + +#[cfg(feature = "web_sys")] +impl From for Vec { + fn from(from: ArrayBuffer) -> Self { + from.0.to_vec() + } +} + +#[cfg(feature = "web_sys")] +impl From for ArrayBuffer { + fn from(from: JsValue) -> Self { + ArrayBuffer(Uint8Array::from(from)) + } +} + /// Type to set cache for fetch. +#[cfg(feature = "std_web")] #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub enum Cache { @@ -36,6 +77,7 @@ pub enum Cache { } /// Type to set credentials for fetch. +#[cfg(feature = "std_web")] #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub enum Credentials { @@ -48,6 +90,7 @@ pub enum Credentials { } /// Type to set mode for fetch. +#[cfg(feature = "std_web")] #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub enum Mode { @@ -60,6 +103,7 @@ pub enum Mode { } /// Type to set redirect behaviour for fetch. +#[cfg(feature = "std_web")] #[derive(Serialize, Debug)] #[serde(rename_all = "kebab-case")] pub enum Redirect { @@ -73,22 +117,48 @@ pub enum Redirect { /// Init options for `fetch()` function call. /// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch -#[derive(Serialize, Default, Debug)] +#[cfg_attr(feature = "std_web", derive(Serialize))] +#[derive(Default, Debug)] pub struct FetchOptions { /// Cache of a fetch request. - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] pub cache: Option, /// Credentials of a fetch request. - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] pub credentials: Option, /// Redirect behaviour of a fetch request. - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] pub redirect: Option, /// Request mode of a fetch request. - #[serde(skip_serializing_if = "Option::is_none")] + #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] pub mode: Option, } +#[cfg(feature = "web_sys")] +impl Into for FetchOptions { + fn into(self) -> RequestInit { + let mut init = RequestInit::new(); + + if let Some(cache) = self.cache { + init.cache(cache); + } + + if let Some(credentials) = self.credentials { + init.credentials(credentials); + } + + if let Some(redirect) = self.redirect { + init.redirect(redirect); + } + + if let Some(mode) = self.mode { + init.mode(mode); + } + + init + } +} + /// Represents errors of a fetch service. #[derive(Debug, Fail)] enum FetchError { @@ -96,9 +166,20 @@ enum FetchError { FailedResponse, } +#[cfg(feature = "web_sys")] +#[derive(Debug)] +struct Handle { + active: Rc, + callbacks: Receiver>, + abort_controller: Option, +} + /// A handle to control sent requests. Can be canceled with a `Task::cancel` call. #[must_use] -pub struct FetchTask(Option); +pub struct FetchTask( + #[cfg(feature = "std_web")] Option, + #[cfg(feature = "web_sys")] Option, +); impl fmt::Debug for FetchTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -223,7 +304,17 @@ impl FetchService { IN: Into, OUT: From, { - fetch_impl::(false, request, None, callback) + #[cfg(feature = "std_web")] + return fetch_impl::(false, request, None, callback); + #[cfg(feature = "web_sys")] + fetch_impl::( + false, + request, + None, + callback, + Into::into, + |v| v.as_string().unwrap(), + ) } /// `fetch` with provided `FetchOptions` object. @@ -266,7 +357,17 @@ impl FetchService { IN: Into, OUT: From, { - fetch_impl::(false, request, Some(options), callback) + #[cfg(feature = "std_web")] + return fetch_impl::(false, request, Some(options), callback); + #[cfg(feature = "web_sys")] + fetch_impl::( + false, + request, + Some(options), + callback, + Into::into, + |v| v.as_string().unwrap(), + ) } /// Fetch the data in binary format. @@ -279,7 +380,17 @@ impl FetchService { IN: Into, OUT: From, { - fetch_impl::, ArrayBuffer>(true, request, None, callback) + #[cfg(feature = "std_web")] + return fetch_impl::, ArrayBuffer>(true, request, None, callback); + #[cfg(feature = "web_sys")] + fetch_impl::, ArrayBuffer, _, _>( + true, + request, + None, + callback, + |v| Uint8Array::from(v.as_slice()).into(), + From::from, + ) } /// Fetch the data in binary format. @@ -293,39 +404,62 @@ impl FetchService { IN: Into, OUT: From, { - fetch_impl::, ArrayBuffer>(true, request, Some(options), callback) + #[cfg(feature = "std_web")] + return fetch_impl::, ArrayBuffer>(true, request, Some(options), callback); + #[cfg(feature = "web_sys")] + fetch_impl::, ArrayBuffer, _, _>( + true, + request, + Some(options), + callback, + |v| Uint8Array::from(v.as_slice()).into(), + From::from, + ) } } -fn fetch_impl( +fn fetch_impl< + IN, + OUT: 'static, + #[cfg(feature = "std_web")] T: JsSerialize, + #[cfg(feature = "web_sys")] T, + #[cfg(feature = "std_web")] X: TryFrom + Into, + #[cfg(feature = "web_sys")] X: Into, + #[cfg(feature = "web_sys")] IC: Fn(T) -> JsValue, + #[cfg(feature = "web_sys")] FC: 'static + Fn(JsValue) -> X, +>( binary: bool, request: Request, options: Option, callback: Callback>, + #[cfg(feature = "web_sys")] into_conversion: IC, + #[cfg(feature = "web_sys")] from_conversion: FC, ) -> FetchTask where IN: Into>, OUT: From>, - T: JsSerialize, - X: TryFrom + Into, { // Consume request as parts and body. let (parts, body) = request.into_parts(); // Map headers into a Js serializable HashMap. - let header_map: HashMap<&str, &str> = parts - .headers - .iter() - .map(|(k, v)| { - ( - k.as_str(), - v.to_str().unwrap_or_else(|_| { - panic!("Unparsable request header {}: {:?}", k.as_str(), v) - }), - ) - }) - .collect(); - + let header_map = parts.headers.iter().map(|(k, v)| { + ( + k.as_str(), + v.to_str() + .unwrap_or_else(|_| panic!("Unparsable request header {}: {:?}", k.as_str(), v)), + ) + }); + #[cfg(feature = "std_web")] + let header_map: HashMap<&str, &str> = header_map.collect(); + #[cfg(feature = "web_sys")] + let header_map = { + let headers = Headers::new().unwrap(); + for (k, v) in header_map { + headers.append(k, v).unwrap(); + } + headers + }; // Formats URI. let uri = format!("{}", parts.uri); let method = parts.method.as_str(); @@ -334,14 +468,32 @@ where // Prepare the response callback. // Notice that the callback signature must match the call from the javascript // side. There is no static check at this point. - let callback = move |success: bool, status: u16, headers: HashMap, data: X| { + let callback = move |#[cfg(feature = "std_web")] success: bool, + #[cfg(feature = "web_sys")] data: Option, + status: u16, + #[cfg(feature = "std_web")] headers: HashMap, + #[cfg(feature = "web_sys")] headers: Headers, + #[cfg(feature = "std_web")] data: X| { let mut response_builder = Response::builder().status(status); - for (key, values) in headers { - response_builder = response_builder.header(key.as_str(), values.as_str()); + #[cfg(feature = "web_sys")] + let headers = js_sys::try_iter(&headers) + .unwrap() + .unwrap() + .map(Result::unwrap) + .map(|entry| { + let entry = Array::from(&entry); + let key = entry.get(0); + let value = entry.get(1); + (key.as_string().unwrap(), value.as_string().unwrap()) + }); + for (key, value) in headers { + response_builder = response_builder.header(key.as_str(), value.as_str()); } // Deserialize and wrap response data into a Text object. - let data = if success { + #[cfg(feature = "std_web")] + let data = Some(data).filter(|_| success); + let data = if let Some(data) = data { Ok(data.into()) } else { Err(FetchError::FailedResponse.into()) @@ -351,6 +503,7 @@ where callback.emit(response); }; + #[cfg(feature = "std_web")] #[allow(clippy::too_many_arguments)] let handle = js! { var body = @{body}; @@ -404,18 +557,103 @@ where }); return handle; }; + #[cfg(feature = "web_sys")] + let handle = { + let mut data = RequestInit::new(); + data.method(method); + data.body(body.map(into_conversion).as_ref()); + data.headers(&header_map); + let request = WebRequest::new_with_str_and_init(&uri, &data).unwrap(); + let active = Rc::new(AtomicBool::new(true)); + let (sender, receiver) = mpsc::channel(); + let active_outer_clone = Rc::clone(&active); + let callback_outer_clone = callback.clone(); + let sender_clone = sender.clone(); + let closure_then = move |response: JsValue| { + let response = WebResponse::from(response); + let promise = if binary { + response.array_buffer() + } else { + response.text() + } + .unwrap(); + let status = response.status(); + let headers = response.headers(); + let active_clone = Rc::clone(&active_outer_clone); + let callback_clone = callback_outer_clone.clone(); + let headers_clone = headers.clone(); + let closure_then = move |data: JsValue| { + let data = from_conversion(data); + if active_clone.load(Ordering::SeqCst) { + active_clone.store(false, Ordering::SeqCst); + callback_clone(Some(data), status, headers_clone); + } + }; + let closure_then = Closure::once(closure_then); + let closure_catch = move |_| { + if active_outer_clone.load(Ordering::SeqCst) { + active_outer_clone.store(false, Ordering::SeqCst); + callback_outer_clone(None, status, headers); + } + }; + let closure_catch = Closure::once(closure_catch); + promise.then(&closure_then).catch(&closure_catch); + sender_clone.send(closure_then).unwrap(); + sender_clone.send(closure_catch).unwrap(); + }; + let closure_then = Closure::once(closure_then); + let active_clone = Rc::clone(&active); + let closure_catch = move |_| { + if active_clone.load(Ordering::SeqCst) { + active_clone.store(false, Ordering::SeqCst); + callback(None, 408, Headers::new().unwrap()); + } + }; + let closure_catch = Closure::wrap(Box::new(closure_catch) as Box); + let abort_controller = AbortController::new().ok(); + let mut init = options.map_or_else(RequestInit::new, Into::into); + if let Some(abort_controller) = &abort_controller { + init.signal(Some(&abort_controller.signal())); + } + let handle = Handle { + active, + callbacks: receiver, + abort_controller, + }; + web_sys::window() + .unwrap() + .fetch_with_request_and_init(&request, &init) + .then(&closure_then) + .catch(&closure_catch); + sender.send(closure_then).unwrap(); + sender.send(closure_catch).unwrap(); + handle + }; FetchTask(Some(handle)) } impl Task for FetchTask { fn is_active(&self) -> bool { if let Some(ref task) = self.0 { - let result = js! { - var the_task = @{task}; - return the_task.active && - (!the_task.abortController || !the_task.abortController.signal.aborted); - }; - result.try_into().unwrap_or(false) + #[cfg(feature = "std_web")] + { + let result = js! { + var the_task = @{task}; + return the_task.active && + (!the_task.abortController || !the_task.abortController.signal.aborted); + }; + result.try_into().unwrap_or(false) + } + #[cfg(feature = "web_sys")] + { + task.active.load(Ordering::SeqCst) + && task + .abort_controller + .as_ref() + .map(|abort_controller| abort_controller.signal().aborted()) + .filter(|value| *value) + .is_none() + } } else { false } @@ -428,6 +666,7 @@ impl Task for FetchTask { .0 .take() .expect("tried to cancel request fetching twice"); + #[cfg(feature = "std_web")] js! { @(no_return) var handle = @{handle}; handle.active = false; @@ -436,6 +675,14 @@ impl Task for FetchTask { handle.abortController.abort(); } } + #[cfg(feature = "web_sys")] + { + handle.active.store(false, Ordering::SeqCst); + if let Some(abort_controller) = handle.abort_controller { + abort_controller.abort(); + } + handle.callbacks.try_iter().for_each(drop); + } } } diff --git a/src/services/interval.rs b/src/services/interval.rs index b2cf7810b49..c9f22fdf0d4 100644 --- a/src/services/interval.rs +++ b/src/services/interval.rs @@ -5,14 +5,31 @@ use super::{to_ms, Task}; use crate::callback::Callback; use std::fmt; use std::time::Duration; +#[cfg(feature = "std_web")] use stdweb::Value; +#[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; +#[cfg(feature = "web_sys")] +use ::{ + std::convert::TryInto, + wasm_bindgen::{closure::Closure, JsCast}, +}; /// A handle which helps to cancel interval. Uses /// [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval). #[must_use] -pub struct IntervalTask(Option); +pub struct IntervalTask( + #[cfg(feature = "std_web")] Option, + #[cfg(feature = "web_sys")] Option, +); + +#[cfg(feature = "web_sys")] +#[derive(Debug)] +struct IntervalTaskInner { + interval_id: i32, + callback: Closure, +} impl fmt::Debug for IntervalTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -37,18 +54,35 @@ impl IntervalService { callback.emit(()); }; let ms = to_ms(duration); - let handle = js! { - var callback = @{callback}; - var action = function() { - callback(); + #[cfg(feature = "std_web")] + { + let handle = js! { + var callback = @{callback}; + var action = function() { + callback(); + }; + var delay = @{ms}; + return { + interval_id: setInterval(action, delay), + callback: callback, + }; }; - var delay = @{ms}; - return { - interval_id: setInterval(action, delay), - callback: callback, - }; - }; - IntervalTask(Some(handle)) + IntervalTask(Some(handle)) + } + #[cfg(feature = "web_sys")] + { + let action = Closure::wrap(Box::new(callback) as Box); + IntervalTask(Some(IntervalTaskInner { + interval_id: web_sys::window() + .unwrap() + .set_interval_with_callback_and_timeout_and_arguments_0( + action.as_ref().unchecked_ref(), + ms.try_into().unwrap(), + ) + .unwrap(), + callback: action, + })) + } } } @@ -58,11 +92,18 @@ impl Task for IntervalTask { } fn cancel(&mut self) { let handle = self.0.take().expect("tried to cancel interval twice"); + #[cfg(feature = "std_web")] js! { @(no_return) var handle = @{handle}; clearInterval(handle.interval_id); handle.callback.drop(); } + #[cfg(feature = "web_sys")] + { + web_sys::window() + .unwrap() + .clear_interval_with_handle(handle.interval_id); + } } } diff --git a/src/services/keyboard.rs b/src/services/keyboard.rs index ba4938a700d..f55b0958445 100644 --- a/src/services/keyboard.rs +++ b/src/services/keyboard.rs @@ -1,8 +1,17 @@ //! Service to register key press event listeners on elements. use crate::callback::Callback; use std::fmt; -use stdweb::web::event::{KeyDownEvent, KeyPressEvent, KeyUpEvent}; -use stdweb::web::{EventListenerHandle, IEventTarget}; +#[cfg(feature = "std_web")] +use stdweb::web::{ + event::{ConcreteEvent, KeyDownEvent, KeyPressEvent, KeyUpEvent}, + EventListenerHandle, IEventTarget, +}; +#[cfg(feature = "web_sys")] +use ::{ + gloo::events::EventListener, + wasm_bindgen::JsCast, + web_sys::{Event, EventTarget, KeyboardEvent}, +}; /// Service for registering callbacks on elements to get keystrokes from the user. /// @@ -19,7 +28,10 @@ pub struct KeyboardService {} /// Handle to the key event listener. /// /// When it goes out of scope, the listener will be removed from the element. -pub struct KeyListenerHandle(Option); +pub struct KeyListenerHandle( + #[cfg(feature = "std_web")] Option, + #[cfg(feature = "web_sys")] EventListener, +); impl fmt::Debug for KeyListenerHandle { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -36,14 +48,20 @@ impl KeyboardService { /// # Warning /// This API has been deprecated in the HTML standard and it is not recommended for use in new projects. /// Consult with the browser compatibility chart in the linked MDN documentation. - pub fn register_key_press( + pub fn register_key_press< + #[cfg(feature = "std_web")] T: IEventTarget, + #[cfg(feature = "web_sys")] T: AsRef, + >( element: &T, - callback: Callback, + #[cfg(feature = "std_web")] callback: Callback, + #[cfg(feature = "web_sys")] callback: Callback, ) -> KeyListenerHandle { - let handle = element.add_event_listener(move |event: KeyPressEvent| { - callback.emit(event); - }); - KeyListenerHandle(Some(handle)) + #[cfg(feature = "std_web")] + { + register_key_impl(element, callback) + } + #[cfg(feature = "web_sys")] + register_key_impl(element, callback, "keypress") } /// Registers a callback that listens to KeyDownEvents on a provided element. @@ -55,14 +73,18 @@ impl KeyboardService { /// This browser feature is relatively new and is set to replace keypress events. /// Not all browsers may support it completely. /// Consult with the browser compatibility chart in the linked MDN documentation. - pub fn register_key_down( + pub fn register_key_down< + #[cfg(feature = "std_web")] T: IEventTarget, + #[cfg(feature = "web_sys")] T: AsRef, + >( element: &T, - callback: Callback, + #[cfg(feature = "std_web")] callback: Callback, + #[cfg(feature = "web_sys")] callback: Callback, ) -> KeyListenerHandle { - let handle = element.add_event_listener(move |event: KeyDownEvent| { - callback.emit(event); - }); - KeyListenerHandle(Some(handle)) + #[cfg(feature = "std_web")] + return register_key_impl(element, callback); + #[cfg(feature = "web_sys")] + register_key_impl(element, callback, "keydown") } /// Registers a callback that listens to KeyUpEvents on a provided element. @@ -74,17 +96,47 @@ impl KeyboardService { /// This browser feature is relatively new and is set to replace keypress events. /// Not all browsers may support it completely. /// Consult with the browser compatibility chart in the linked MDN documentation. - pub fn register_key_up( + pub fn register_key_up< + #[cfg(feature = "std_web")] T: IEventTarget, + #[cfg(feature = "web_sys")] T: AsRef, + >( element: &T, - callback: Callback, + #[cfg(feature = "std_web")] callback: Callback, + #[cfg(feature = "web_sys")] callback: Callback, ) -> KeyListenerHandle { - let handle = element.add_event_listener(move |event: KeyUpEvent| { - callback.emit(event); - }); - KeyListenerHandle(Some(handle)) + #[cfg(feature = "std_web")] + return register_key_impl(element, callback); + #[cfg(feature = "web_sys")] + register_key_impl(element, callback, "keyup") } } +#[cfg(feature = "std_web")] +fn register_key_impl(element: &T, callback: Callback) -> KeyListenerHandle { + let handle = element.add_event_listener(move |event: E| { + callback.emit(event); + }); + KeyListenerHandle(Some(handle)) +} + +#[cfg(feature = "web_sys")] +fn register_key_impl>( + element: &T, + callback: Callback, + event: &'static str, +) -> KeyListenerHandle { + let listener = move |event: &Event| { + let event = event + .dyn_ref::() + .expect("wrong event type") + .clone(); + callback.emit(event); + }; + + KeyListenerHandle(EventListener::new(element.as_ref(), event, listener)) +} + +#[cfg(feature = "std_web")] impl Drop for KeyListenerHandle { fn drop(&mut self) { if let Some(handle) = self.0.take() { diff --git a/src/services/reader.rs b/src/services/reader.rs index c1fd144b098..e5e83ac5914 100644 --- a/src/services/reader.rs +++ b/src/services/reader.rs @@ -4,12 +4,27 @@ use super::Task; use crate::callback::Callback; use std::cmp; use std::fmt; -use stdweb::unstable::TryInto; -use stdweb::web::event::LoadEndEvent; +#[cfg(feature = "std_web")] pub use stdweb::web::{Blob, File, IBlob}; -use stdweb::web::{FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, TypedArray}; +#[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; +#[cfg(feature = "std_web")] +use stdweb::{ + unstable::TryInto, + web::{ + event::LoadEndEvent, FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, + TypedArray, + }, +}; +#[cfg(feature = "web_sys")] +pub use web_sys::{Blob, File}; +#[cfg(feature = "web_sys")] +use ::{ + gloo::events::EventListener, + js_sys::Uint8Array, + web_sys::{Event, FileReader}, +}; /// Struct that represents data of a file. #[derive(Clone, Debug)] @@ -52,24 +67,47 @@ impl ReaderService { /// Reads all bytes from a file and returns them with a callback. pub fn read_file(&mut self, file: File, callback: Callback) -> ReaderTask { let file_reader = FileReader::new(); + #[cfg(feature = "web_sys")] + let file_reader = file_reader.unwrap(); let reader = file_reader.clone(); let name = file.name(); - file_reader.add_event_listener(move |_event: LoadEndEvent| match reader.result() { - Some(FileReaderResult::String(_)) => { - unreachable!(); + let callback = move |#[cfg(feature = "std_web")] _event: LoadEndEvent, + #[cfg(feature = "web_sys")] _event: &Event| { + #[cfg(feature = "std_web")] + match reader.result() { + Some(FileReaderResult::String(_)) => { + unreachable!(); + } + Some(FileReaderResult::ArrayBuffer(buffer)) => { + let array: TypedArray = buffer.into(); + let data = FileData { + name: name.clone(), + content: array.to_vec(), + }; + callback.emit(data); + } + None => {} } - Some(FileReaderResult::ArrayBuffer(buffer)) => { - let array: TypedArray = buffer.into(); + #[cfg(feature = "web_sys")] + { + let array = Uint8Array::new(&reader.result().unwrap()); let data = FileData { name: name.clone(), content: array.to_vec(), }; callback.emit(data); } - None => {} - }); + }; + #[cfg(feature = "std_web")] + file_reader.add_event_listener(callback); + #[cfg(feature = "web_sys")] + let listener = EventListener::new(&file_reader, "loadend", callback); file_reader.read_as_array_buffer(&file).unwrap(); - ReaderTask { file_reader } + ReaderTask { + file_reader, + #[cfg(feature = "web_sys")] + listener, + } } /// Reads data chunks from a file and returns them with a callback. @@ -80,11 +118,18 @@ impl ReaderService { chunk_size: usize, ) -> ReaderTask { let file_reader = FileReader::new(); + #[cfg(feature = "web_sys")] + let file_reader = file_reader.unwrap(); let name = file.name(); let mut position = 0; + #[cfg(feature = "std_web")] let total_size = file.len() as usize; + #[cfg(feature = "web_sys")] + let total_size = file.size() as usize; let reader = file_reader.clone(); - file_reader.add_event_listener(move |_event: LoadEndEvent| { + let callback = move |#[cfg(feature = "std_web")] _event: LoadEndEvent, + #[cfg(feature = "web_sys")] _event: &Event| { + #[cfg(feature = "std_web")] match reader.result() { // This branch is used to start reading Some(FileReaderResult::String(_)) => { @@ -102,31 +147,63 @@ impl ReaderService { } None => {} } + #[cfg(feature = "web_sys")] + { + let result = reader.result().unwrap(); + + if result.is_string() { + let started = FileChunk::Started { name: name.clone() }; + callback.emit(started); + } else { + let array = Uint8Array::new(&result); + let chunk = FileChunk::DataChunk { + data: array.to_vec(), + progress: position as f32 / total_size as f32, + }; + callback.emit(chunk); + } + } // Read the next chunk if position < total_size { - let file = &file; let from = position; let to = cmp::min(position + chunk_size, total_size); position = to; + #[cfg(feature = "std_web")] // TODO Implement `slice` method in `stdweb` - let blob: Blob = (js! { - return @{file}.slice(@{from as u32}, @{to as u32}); - }) - .try_into() - .unwrap(); + let blob: Blob = { + let file = &file; + (js! { + return @{file}.slice(@{from as u32}, @{to as u32}); + }) + .try_into() + .unwrap() + }; + #[cfg(feature = "web_sys")] + let blob = file.slice_with_i32_and_i32(from as _, to as _).unwrap(); reader.read_as_array_buffer(&blob).unwrap(); } else { let finished = FileChunk::Finished; callback.emit(finished); } - }); + }; + #[cfg(feature = "std_web")] + file_reader.add_event_listener(callback); + #[cfg(feature = "web_sys")] + let listener = EventListener::new(&file_reader, "loadend", callback); + #[cfg(feature = "std_web")] let blob: Blob = (js! { return (new Blob()); }) .try_into() .unwrap(); + #[cfg(feature = "web_sys")] + let blob = Blob::new().unwrap(); file_reader.read_as_text(&blob).unwrap(); - ReaderTask { file_reader } + ReaderTask { + file_reader, + #[cfg(feature = "web_sys")] + listener, + } } } @@ -134,6 +211,9 @@ impl ReaderService { #[must_use] pub struct ReaderTask { file_reader: FileReader, + #[cfg(feature = "web_sys")] + #[allow(dead_code)] + listener: EventListener, } impl fmt::Debug for ReaderTask { @@ -144,7 +224,14 @@ impl fmt::Debug for ReaderTask { impl Task for ReaderTask { fn is_active(&self) -> bool { - self.file_reader.ready_state() == FileReaderReadyState::Loading + #[cfg(feature = "std_web")] + { + self.file_reader.ready_state() == FileReaderReadyState::Loading + } + #[cfg(feature = "web_sys")] + { + self.file_reader.ready_state() == 1 + } } fn cancel(&mut self) { diff --git a/src/services/render.rs b/src/services/render.rs index b5d244a84b8..6f3bead4fe6 100644 --- a/src/services/render.rs +++ b/src/services/render.rs @@ -4,14 +4,27 @@ use crate::callback::Callback; use crate::services::Task; use std::fmt; -use stdweb::unstable::TryInto; -use stdweb::Value; +#[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; +#[cfg(feature = "std_web")] +use stdweb::{unstable::TryInto, Value}; +#[cfg(feature = "web_sys")] +use wasm_bindgen::{closure::Closure, JsCast, JsValue}; /// A handle to cancel a render task. #[must_use] -pub struct RenderTask(Option); +pub struct RenderTask( + #[cfg(feature = "std_web")] Option, + #[cfg(feature = "web_sys")] Option, +); + +#[cfg(feature = "web_sys")] +struct RenderTaskInner { + render_id: i32, + #[allow(dead_code)] + callback: Closure, +} impl fmt::Debug for RenderTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -31,13 +44,18 @@ impl RenderService { /// Request animation frame. Callback will be notified when frame should be rendered. pub fn request_animation_frame(&mut self, callback: Callback) -> RenderTask { - let callback = move |v| { + let callback = move |#[cfg(feature = "std_web")] v, + #[cfg(feature = "web_sys")] v: JsValue| { + #[cfg(feature = "std_web")] let time: f64 = match v { Value::Number(n) => n.try_into().unwrap(), _ => 0.0, }; + #[cfg(feature = "web_sys")] + let time = v.as_f64().unwrap_or(0.); callback.emit(time); }; + #[cfg(feature = "std_web")] let handle = js! { var callback = @{callback}; var action = function(time) { @@ -49,6 +67,18 @@ impl RenderService { callback: callback, }; }; + #[cfg(feature = "web_sys")] + let handle = { + let callback = Closure::wrap(Box::new(callback) as Box); + let render_id = web_sys::window() + .unwrap() + .request_animation_frame(callback.as_ref().unchecked_ref()) + .unwrap(); + RenderTaskInner { + render_id, + callback, + } + }; RenderTask(Some(handle)) } } @@ -59,11 +89,17 @@ impl Task for RenderTask { } fn cancel(&mut self) { let handle = self.0.take().expect("tried to cancel render twice"); + #[cfg(feature = "std_web")] js! { @(no_return) var handle = @{handle}; cancelAnimationFrame(handle.render_id); handle.callback.drop(); } + #[cfg(feature = "web_sys")] + web_sys::window() + .unwrap() + .cancel_animation_frame(handle.render_id) + .unwrap(); } } diff --git a/src/services/resize.rs b/src/services/resize.rs index fd3593795f4..3b968a8fe5c 100644 --- a/src/services/resize.rs +++ b/src/services/resize.rs @@ -1,11 +1,17 @@ //! This module contains the implementation of a service that listens for browser window resize events. use std::fmt; -use stdweb::Value; +#[cfg(feature = "std_web")] use stdweb::{ js, web::{window, Window}, + Value, }; use yew::callback::Callback; +#[cfg(feature = "web_sys")] +use ::{ + gloo::events::EventListener, + web_sys::{Event, Window}, +}; /// A service that fires events when the browser window resizes. #[derive(Default, Debug)] @@ -13,7 +19,10 @@ pub struct ResizeService {} /// A handle to the event listener for resize events. #[must_use] -pub struct ResizeTask(Option); +pub struct ResizeTask( + #[cfg(feature = "std_web")] Option, + #[cfg(feature = "web_sys")] EventListener, +); impl fmt::Debug for ResizeTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -33,10 +42,16 @@ pub struct WindowDimensions { impl WindowDimensions { /// Gets the dimensions of the browser window. pub fn get_dimensions(window: &Window) -> Self { - WindowDimensions { - width: window.inner_width(), - height: window.inner_height(), - } + let width = window.inner_width(); + let height = window.inner_height(); + #[cfg(feature = "web_sys")] + let (width, height) = { + ( + width.unwrap().as_f64().unwrap() as _, + height.unwrap().as_f64().unwrap() as _, + ) + }; + WindowDimensions { width, height } } } @@ -48,22 +63,29 @@ impl ResizeService { /// Register a callback that will be called when the browser window resizes. pub fn register(&mut self, callback: Callback) -> ResizeTask { - let callback = move || { + let callback = move |#[cfg(feature = "web_sys")] _event: &Event| { + #[cfg(feature = "std_web")] let window = window(); + #[cfg(feature = "web_sys")] + let window = web_sys::window().unwrap(); let dimensions = WindowDimensions::get_dimensions(&window); callback.emit(dimensions); }; - let handle = js! { + #[cfg(feature = "std_web")] + let handle = Some(js! { var callback = @{callback}; var action = function() { callback(); }; return window.addEventListener("resize", action); - }; - ResizeTask(Some(handle)) + }); + #[cfg(feature = "web_sys")] + let handle = EventListener::new(&web_sys::window().unwrap(), "resize", callback); + ResizeTask(handle) } } +#[cfg(feature = "std_web")] impl Drop for ResizeTask { fn drop(&mut self) { let handle = self.0.take().expect("Resize task already empty."); diff --git a/src/services/storage.rs b/src/services/storage.rs index e559740a41c..477221b7448 100644 --- a/src/services/storage.rs +++ b/src/services/storage.rs @@ -4,7 +4,10 @@ use crate::format::Text; use failure::Fail; use std::fmt; +#[cfg(feature = "std_web")] use stdweb::web::{window, Storage}; +#[cfg(feature = "web_sys")] +use web_sys::Storage; /// Represents errors of a storage. #[derive(Debug, Fail)] @@ -37,11 +40,17 @@ impl StorageService { /// Creates a new storage service instance with specified storage area. pub fn new(area: Area) -> Self { let storage = { + #[cfg(feature = "std_web")] + let window = window(); + #[cfg(feature = "web_sys")] + let window = web_sys::window().unwrap(); match area { - Area::Local => window().local_storage(), - Area::Session => window().session_storage(), + Area::Local => window.local_storage(), + Area::Session => window.session_storage(), } }; + #[cfg(feature = "web_sys")] + let storage = storage.unwrap().unwrap(); StorageService { storage } } @@ -51,9 +60,11 @@ impl StorageService { T: Into, { if let Ok(data) = value.into() { - self.storage - .insert(key, &data) - .expect("can't insert value to a storage"); + #[cfg(feature = "std_web")] + let result = self.storage.insert(key, &data); + #[cfg(feature = "web_sys")] + let result = self.storage.set_item(key, &data); + result.expect("can't insert value to a storage"); } } @@ -62,15 +73,19 @@ impl StorageService { where T: From, { - let data = self - .storage - .get(key) - .ok_or_else(|| StorageError::CantRestore.into()); + #[cfg(feature = "std_web")] + let data = self.storage.get(key); + #[cfg(feature = "web_sys")] + let data = self.storage.get_item(key).unwrap(); + let data = data.ok_or_else(|| StorageError::CantRestore.into()); T::from(data) } /// Removes value from the storage. pub fn remove(&mut self, key: &str) { + #[cfg(feature = "std_web")] self.storage.remove(key); + #[cfg(feature = "web_sys")] + self.storage.remove_item(key).unwrap(); } } diff --git a/src/services/timeout.rs b/src/services/timeout.rs index eb8450ecbfb..f5719a023df 100644 --- a/src/services/timeout.rs +++ b/src/services/timeout.rs @@ -3,15 +3,22 @@ use super::{to_ms, Task}; use crate::callback::Callback; +#[cfg(feature = "web_sys")] +use gloo::timers::callback::Timeout; use std::fmt; use std::time::Duration; +#[cfg(feature = "std_web")] use stdweb::Value; +#[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; /// A handle to cancel a timeout task. #[must_use] -pub struct TimeoutTask(Option); +pub struct TimeoutTask( + #[cfg(feature = "std_web")] Option, + #[cfg(feature = "web_sys")] Option, +); impl fmt::Debug for TimeoutTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { @@ -35,6 +42,7 @@ impl TimeoutService { callback.emit(()); }; let ms = to_ms(duration); + #[cfg(feature = "std_web")] let handle = js! { var callback = @{callback}; var action = function() { @@ -47,6 +55,8 @@ impl TimeoutService { callback: callback, }; }; + #[cfg(feature = "web_sys")] + let handle = Timeout::new(ms, callback); TimeoutTask(Some(handle)) } } @@ -57,11 +67,14 @@ impl Task for TimeoutTask { } fn cancel(&mut self) { let handle = self.0.take().expect("tried to cancel timeout twice"); + #[cfg(feature = "std_web")] js! { @(no_return) var handle = @{handle}; clearTimeout(handle.timeout_id); handle.callback.drop(); } + #[cfg(feature = "web_sys")] + handle.cancel(); } } diff --git a/src/services/websocket.rs b/src/services/websocket.rs index 548ee320cee..cba836b5264 100644 --- a/src/services/websocket.rs +++ b/src/services/websocket.rs @@ -5,9 +5,21 @@ use super::Task; use crate::callback::Callback; use crate::format::{Binary, Text}; use std::fmt; -use stdweb::traits::IMessageEvent; -use stdweb::web::event::{SocketCloseEvent, SocketErrorEvent, SocketMessageEvent, SocketOpenEvent}; -use stdweb::web::{IEventTarget, SocketBinaryType, SocketReadyState, WebSocket}; +#[cfg(feature = "std_web")] +use stdweb::{ + traits::IMessageEvent, + web::{ + event::{SocketCloseEvent, SocketErrorEvent, SocketMessageEvent, SocketOpenEvent}, + IEventTarget, SocketBinaryType, SocketReadyState, WebSocket, + }, +}; +#[cfg(feature = "web_sys")] +use ::{ + gloo::events::EventListener, + js_sys::Uint8Array, + wasm_bindgen::JsCast, + web_sys::{BinaryType, Event, MessageEvent, WebSocket}, +}; /// A status of a websocket connection. Used for status notification. #[derive(Debug)] @@ -25,6 +37,9 @@ pub enum WebSocketStatus { pub struct WebSocketTask { ws: WebSocket, notification: Callback, + #[cfg(feature = "web_sys")] + #[allow(dead_code)] + listeners: [EventListener; 4], } impl fmt::Debug for WebSocketTask { @@ -60,32 +75,79 @@ impl WebSocketService { } let ws = ws.unwrap(); + #[cfg(feature = "std_web")] ws.set_binary_type(SocketBinaryType::ArrayBuffer); + #[cfg(feature = "web_sys")] + ws.set_binary_type(BinaryType::Arraybuffer); let notify = notification.clone(); - ws.add_event_listener(move |_: SocketOpenEvent| { + let listener = move |#[cfg(feature = "std_web")] _: SocketOpenEvent, + #[cfg(feature = "web_sys")] _: &Event| { notify.emit(WebSocketStatus::Opened); - }); + }; + #[cfg(feature = "std_web")] + ws.add_event_listener(listener); + #[cfg(feature = "web_sys")] + let listener_open = EventListener::new(&ws, "open", listener); let notify = notification.clone(); - ws.add_event_listener(move |_: SocketCloseEvent| { + let listener = move |#[cfg(feature = "std_web")] _: SocketCloseEvent, + #[cfg(feature = "web_sys")] _: &Event| { notify.emit(WebSocketStatus::Closed); - }); + }; + #[cfg(feature = "std_web")] + ws.add_event_listener(listener); + #[cfg(feature = "web_sys")] + let listener_close = EventListener::new(&ws, "close", listener); let notify = notification.clone(); - ws.add_event_listener(move |_: SocketErrorEvent| { + let listener = move |#[cfg(feature = "std_web")] _: SocketErrorEvent, + #[cfg(feature = "web_sys")] _: &Event| { notify.emit(WebSocketStatus::Error); - }); - ws.add_event_listener(move |event: SocketMessageEvent| { - if let Some(bytes) = event.data().into_array_buffer() { - let bytes: Vec = bytes.into(); - let data = Ok(bytes); + }; + #[cfg(feature = "std_web")] + ws.add_event_listener(listener); + #[cfg(feature = "web_sys")] + let listener_error = EventListener::new(&ws, "error", listener); + let listener = move |#[cfg(feature = "std_web")] event: SocketMessageEvent, + #[cfg(feature = "web_sys")] event: &Event| { + #[cfg(feature = "web_sys")] + let data = event.dyn_ref::().unwrap().data(); + #[cfg(feature = "std_web")] + let text = event.data().into_text(); + #[cfg(feature = "web_sys")] + let text = data.as_string(); + #[cfg(feature = "std_web")] + let bytes = event.data().into_array_buffer(); + #[cfg(feature = "web_sys")] + let bytes = Some(data); + + if let Some(text) = text { + let data = Ok(text); let out = OUT::from(data); callback.emit(out); - } else if let Some(text) = event.data().into_text() { - let data = Ok(text); + } else if let Some(bytes) = bytes { + #[cfg(feature = "std_web")] + let bytes: Vec = bytes.into(); + #[cfg(feature = "web_sys")] + let bytes = Uint8Array::from(bytes).to_vec(); + let data = Ok(bytes); let out = OUT::from(data); callback.emit(out); } - }); - Ok(WebSocketTask { ws, notification }) + }; + #[cfg(feature = "std_web")] + ws.add_event_listener(listener); + #[cfg(feature = "web_sys")] + let listener_message = EventListener::new(&ws, "message", listener); + Ok(WebSocketTask { + ws, + notification, + #[cfg(feature = "web_sys")] + listeners: [ + listener_open, + listener_close, + listener_error, + listener_message, + ], + }) } } @@ -96,7 +158,12 @@ impl WebSocketTask { IN: Into, { if let Ok(body) = data.into() { - if self.ws.send_text(&body).is_err() { + #[cfg(feature = "std_web")] + let result = self.ws.send_text(&body); + #[cfg(feature = "web_sys")] + let result = self.ws.send_with_str(&body); + + if result.is_err() { self.notification.emit(WebSocketStatus::Error); } } @@ -108,7 +175,15 @@ impl WebSocketTask { IN: Into, { if let Ok(body) = data.into() { - if self.ws.send_bytes(&body).is_err() { + #[cfg(feature = "std_web")] + let result = self.ws.send_bytes(&body); + #[cfg(feature = "web_sys")] + let result = { + let mut body = body; + self.ws.send_with_u8_array(&mut body) + }; + + if result.is_err() { self.notification.emit(WebSocketStatus::Error); } } @@ -117,10 +192,20 @@ impl WebSocketTask { impl Task for WebSocketTask { fn is_active(&self) -> bool { - self.ws.ready_state() == SocketReadyState::Open + #[cfg(feature = "std_web")] + { + self.ws.ready_state() == SocketReadyState::Open + } + #[cfg(feature = "web_sys")] + { + self.ws.ready_state() == WebSocket::OPEN + } } fn cancel(&mut self) { + #[cfg(feature = "std_web")] self.ws.close(); + #[cfg(feature = "web_sys")] + self.ws.close().unwrap(); } } From c8ac5c822b4955b7b25ce516403a16be77ea42b2 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Tue, 31 Dec 2019 16:53:14 +0100 Subject: [PATCH 03/11] Some polish. --- src/services/fetch.rs | 21 +++++++++++++++------ src/services/interval.rs | 37 +++++-------------------------------- src/services/keyboard.rs | 5 ++++- src/services/reader.rs | 2 +- src/services/timeout.rs | 3 +-- 5 files changed, 26 insertions(+), 42 deletions(-) diff --git a/src/services/fetch.rs b/src/services/fetch.rs index e7c7afcc807..9f8d49e940f 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -9,6 +9,11 @@ use std::fmt; #[allow(unused_imports)] use stdweb::{_js_impl, js}; #[cfg(feature = "web_sys")] +pub use web_sys::{ + RequestCache as Cache, RequestCredentials as Credentials, RequestMode as Mode, + RequestRedirect as Redirect, +}; +#[cfg(feature = "web_sys")] use ::{ js_sys::{Array, Uint8Array}, std::{ @@ -20,9 +25,7 @@ use ::{ }, wasm_bindgen::{closure::Closure, JsValue}, web_sys::{ - AbortController, Headers, Request as WebRequest, RequestCache as Cache, - RequestCredentials as Credentials, RequestInit, RequestMode as Mode, - RequestRedirect as Redirect, Response as WebResponse, + AbortController, Headers, Request as WebRequest, RequestInit, Response as WebResponse, }, }; #[cfg(feature = "std_web")] @@ -305,7 +308,9 @@ impl FetchService { OUT: From, { #[cfg(feature = "std_web")] - return fetch_impl::(false, request, None, callback); + { + fetch_impl::(false, request, None, callback) + } #[cfg(feature = "web_sys")] fetch_impl::( false, @@ -358,7 +363,9 @@ impl FetchService { OUT: From, { #[cfg(feature = "std_web")] - return fetch_impl::(false, request, Some(options), callback); + { + fetch_impl::(false, request, Some(options), callback) + } #[cfg(feature = "web_sys")] fetch_impl::( false, @@ -381,7 +388,9 @@ impl FetchService { OUT: From, { #[cfg(feature = "std_web")] - return fetch_impl::, ArrayBuffer>(true, request, None, callback); + { + fetch_impl::, ArrayBuffer>(true, request, None, callback) + } #[cfg(feature = "web_sys")] fetch_impl::, ArrayBuffer, _, _>( true, diff --git a/src/services/interval.rs b/src/services/interval.rs index c9f22fdf0d4..18a5eed9f33 100644 --- a/src/services/interval.rs +++ b/src/services/interval.rs @@ -3,6 +3,8 @@ use super::{to_ms, Task}; use crate::callback::Callback; +#[cfg(feature = "web_sys")] +use gloo::timers::callback::Interval; use std::fmt; use std::time::Duration; #[cfg(feature = "std_web")] @@ -10,27 +12,15 @@ use stdweb::Value; #[cfg(feature = "std_web")] #[allow(unused_imports)] use stdweb::{_js_impl, js}; -#[cfg(feature = "web_sys")] -use ::{ - std::convert::TryInto, - wasm_bindgen::{closure::Closure, JsCast}, -}; /// A handle which helps to cancel interval. Uses /// [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval). #[must_use] pub struct IntervalTask( #[cfg(feature = "std_web")] Option, - #[cfg(feature = "web_sys")] Option, + #[cfg(feature = "web_sys")] Option, ); -#[cfg(feature = "web_sys")] -#[derive(Debug)] -struct IntervalTaskInner { - interval_id: i32, - callback: Closure, -} - impl fmt::Debug for IntervalTask { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { f.write_str("IntervalTask") @@ -70,19 +60,7 @@ impl IntervalService { IntervalTask(Some(handle)) } #[cfg(feature = "web_sys")] - { - let action = Closure::wrap(Box::new(callback) as Box); - IntervalTask(Some(IntervalTaskInner { - interval_id: web_sys::window() - .unwrap() - .set_interval_with_callback_and_timeout_and_arguments_0( - action.as_ref().unchecked_ref(), - ms.try_into().unwrap(), - ) - .unwrap(), - callback: action, - })) - } + IntervalTask(Some(Interval::new(ms, callback))) } } @@ -91,6 +69,7 @@ impl Task for IntervalTask { self.0.is_some() } fn cancel(&mut self) { + #[cfg_attr(feature = "web_sys", allow(unused_variables))] let handle = self.0.take().expect("tried to cancel interval twice"); #[cfg(feature = "std_web")] js! { @(no_return) @@ -98,12 +77,6 @@ impl Task for IntervalTask { clearInterval(handle.interval_id); handle.callback.drop(); } - #[cfg(feature = "web_sys")] - { - web_sys::window() - .unwrap() - .clear_interval_with_handle(handle.interval_id); - } } } diff --git a/src/services/keyboard.rs b/src/services/keyboard.rs index f55b0958445..02d923e7c42 100644 --- a/src/services/keyboard.rs +++ b/src/services/keyboard.rs @@ -112,7 +112,10 @@ impl KeyboardService { } #[cfg(feature = "std_web")] -fn register_key_impl(element: &T, callback: Callback) -> KeyListenerHandle { +fn register_key_impl( + element: &T, + callback: Callback, +) -> KeyListenerHandle { let handle = element.add_event_listener(move |event: E| { callback.emit(event); }); diff --git a/src/services/reader.rs b/src/services/reader.rs index e5e83ac5914..036d9088066 100644 --- a/src/services/reader.rs +++ b/src/services/reader.rs @@ -230,7 +230,7 @@ impl Task for ReaderTask { } #[cfg(feature = "web_sys")] { - self.file_reader.ready_state() == 1 + self.file_reader.ready_state() == FileReader::LOADING } } diff --git a/src/services/timeout.rs b/src/services/timeout.rs index f5719a023df..1e5440d0cc9 100644 --- a/src/services/timeout.rs +++ b/src/services/timeout.rs @@ -66,6 +66,7 @@ impl Task for TimeoutTask { self.0.is_some() } fn cancel(&mut self) { + #[cfg_attr(feature = "web_sys", allow(unused_variables))] let handle = self.0.take().expect("tried to cancel timeout twice"); #[cfg(feature = "std_web")] js! { @(no_return) @@ -73,8 +74,6 @@ impl Task for TimeoutTask { clearTimeout(handle.timeout_id); handle.callback.drop(); } - #[cfg(feature = "web_sys")] - handle.cancel(); } } From 45fda39e7d2b3aa117359f6b0486744a5441e8fd Mon Sep 17 00:00:00 2001 From: daxpedda Date: Wed, 1 Jan 2020 23:29:04 +0100 Subject: [PATCH 04/11] Fix `ArrayBuffer` to `Uint8Array` conversions. --- src/services/fetch.rs | 2 +- src/services/reader.rs | 4 ++-- src/services/websocket.rs | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/services/fetch.rs b/src/services/fetch.rs index 9f8d49e940f..0db7c34942f 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -55,7 +55,7 @@ impl From for Vec { #[cfg(feature = "web_sys")] impl From for ArrayBuffer { fn from(from: JsValue) -> Self { - ArrayBuffer(Uint8Array::from(from)) + ArrayBuffer(Uint8Array::new_with_byte_offset(&from, 0)) } } diff --git a/src/services/reader.rs b/src/services/reader.rs index 036d9088066..7b3fc8f99c0 100644 --- a/src/services/reader.rs +++ b/src/services/reader.rs @@ -90,7 +90,7 @@ impl ReaderService { } #[cfg(feature = "web_sys")] { - let array = Uint8Array::new(&reader.result().unwrap()); + let array = Uint8Array::new_with_byte_offset(&reader.result().unwrap(), 0); let data = FileData { name: name.clone(), content: array.to_vec(), @@ -155,7 +155,7 @@ impl ReaderService { let started = FileChunk::Started { name: name.clone() }; callback.emit(started); } else { - let array = Uint8Array::new(&result); + let array = Uint8Array::new_with_byte_offset(&result, 0); let chunk = FileChunk::DataChunk { data: array.to_vec(), progress: position as f32 / total_size as f32, diff --git a/src/services/websocket.rs b/src/services/websocket.rs index cba836b5264..7f4e3f5c437 100644 --- a/src/services/websocket.rs +++ b/src/services/websocket.rs @@ -127,7 +127,7 @@ impl WebSocketService { #[cfg(feature = "std_web")] let bytes: Vec = bytes.into(); #[cfg(feature = "web_sys")] - let bytes = Uint8Array::from(bytes).to_vec(); + let bytes = Uint8Array::new_with_byte_offset(&bytes, 0).to_vec(); let data = Ok(bytes); let out = OUT::from(data); callback.emit(out); From fd2df8bd580a45a867d6185c7ed9f6646735f635 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sat, 4 Jan 2020 00:57:23 +0100 Subject: [PATCH 05/11] Fix aborting fetch leading to error and some polish. --- src/services/console.rs | 12 ++++++------ src/services/fetch.rs | 30 ++++++++++++++++-------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/services/console.rs b/src/services/console.rs index 9f9f73c725c..970b8ddcd28 100644 --- a/src/services/console.rs +++ b/src/services/console.rs @@ -4,7 +4,7 @@ #[allow(unused_imports)] use stdweb::{_js_impl, js}; #[cfg(feature = "web_sys")] -use web_sys::console; +use ::{wasm_bindgen::JsValue, web_sys::console}; /// A service to use methods of a /// [Console](https://developer.mozilla.org/en-US/docs/Web/API/Console). @@ -23,7 +23,7 @@ impl ConsoleService { #[cfg(feature = "std_web")] js! { @(no_return) console.log(@{message}); } #[cfg(feature = "web_sys")] - console::log_1(&String::from(message).into()); + console::log_1(&JsValue::from_str(message)); } /// [console.warn](https://developer.mozilla.org/en-US/docs/Web/API/Console/warn) @@ -32,7 +32,7 @@ impl ConsoleService { #[cfg(feature = "std_web")] js! { @(no_return) console.warn(@{message}); } #[cfg(feature = "web_sys")] - console::warn_1(&String::from(message).into()); + console::warn_1(&JsValue::from_str(message)); } /// [console.info](https://developer.mozilla.org/en-US/docs/Web/API/Console/info) @@ -41,7 +41,7 @@ impl ConsoleService { #[cfg(feature = "std_web")] js! { @(no_return) console.info(@{message}); } #[cfg(feature = "web_sys")] - console::info_1(&String::from(message).into()); + console::info_1(&JsValue::from_str(message)); } /// [console.error](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) @@ -50,7 +50,7 @@ impl ConsoleService { #[cfg(feature = "std_web")] js! { @(no_return) console.error(@{message}); } #[cfg(feature = "web_sys")] - console::error_1(&String::from(message).into()); + console::error_1(&JsValue::from_str(message)); } /// [console.debug](https://developer.mozilla.org/en-US/docs/Web/API/Console/debug) @@ -59,7 +59,7 @@ impl ConsoleService { #[cfg(feature = "std_web")] js! { @(no_return) console.debug(@{message}); } #[cfg(feature = "web_sys")] - console::debug_1(&String::from(message).into()); + console::debug_1(&JsValue::from_str(message)); } /// [console.count_named](https://developer.mozilla.org/en-US/docs/Web/API/Console/count_named) diff --git a/src/services/fetch.rs b/src/services/fetch.rs index 0db7c34942f..dfb94ef5879 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -15,7 +15,7 @@ pub use web_sys::{ }; #[cfg(feature = "web_sys")] use ::{ - js_sys::{Array, Uint8Array}, + js_sys::{Array, Promise, Uint8Array}, std::{ rc::Rc, sync::{ @@ -175,6 +175,7 @@ struct Handle { active: Rc, callbacks: Receiver>, abort_controller: Option, + promise: Promise, } /// A handle to control sent requests. Can be canceled with a `Task::cancel` call. @@ -593,15 +594,13 @@ where let headers_clone = headers.clone(); let closure_then = move |data: JsValue| { let data = from_conversion(data); - if active_clone.load(Ordering::SeqCst) { - active_clone.store(false, Ordering::SeqCst); + if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { callback_clone(Some(data), status, headers_clone); } }; let closure_then = Closure::once(closure_then); let closure_catch = move |_| { - if active_outer_clone.load(Ordering::SeqCst) { - active_outer_clone.store(false, Ordering::SeqCst); + if active_outer_clone.compare_and_swap(true, false, Ordering::SeqCst) { callback_outer_clone(None, status, headers); } }; @@ -613,8 +612,7 @@ where let closure_then = Closure::once(closure_then); let active_clone = Rc::clone(&active); let closure_catch = move |_| { - if active_clone.load(Ordering::SeqCst) { - active_clone.store(false, Ordering::SeqCst); + if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { callback(None, 408, Headers::new().unwrap()); } }; @@ -624,19 +622,19 @@ where if let Some(abort_controller) = &abort_controller { init.signal(Some(&abort_controller.signal())); } - let handle = Handle { - active, - callbacks: receiver, - abort_controller, - }; - web_sys::window() + let promise = web_sys::window() .unwrap() .fetch_with_request_and_init(&request, &init) .then(&closure_then) .catch(&closure_catch); sender.send(closure_then).unwrap(); sender.send(closure_catch).unwrap(); - handle + Handle { + active, + callbacks: receiver, + abort_controller, + promise, + } }; FetchTask(Some(handle)) } @@ -686,7 +684,11 @@ impl Task for FetchTask { } #[cfg(feature = "web_sys")] { + thread_local! { + static CATCH: Closure = Closure::wrap(Box::new(|_| ()) as Box); + } handle.active.store(false, Ordering::SeqCst); + CATCH.with(|c| handle.promise.catch(&c)); if let Some(abort_controller) = handle.abort_controller { abort_controller.abort(); } From 8d49f0be26fcd058f89e78fffd8ca8538daffca1 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 9 Jan 2020 10:32:20 +0100 Subject: [PATCH 06/11] Replaced some `unwrap`s with `expect`s. --- src/services/storage.rs | 6 ++++-- src/services/websocket.rs | 2 +- 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/services/storage.rs b/src/services/storage.rs index 477221b7448..a2fcc684907 100644 --- a/src/services/storage.rs +++ b/src/services/storage.rs @@ -7,7 +7,7 @@ use std::fmt; #[cfg(feature = "std_web")] use stdweb::web::{window, Storage}; #[cfg(feature = "web_sys")] -use web_sys::Storage; +use ::{wasm_bindgen::JsValue, web_sys::Storage}; /// Represents errors of a storage. #[derive(Debug, Fail)] @@ -50,7 +50,9 @@ impl StorageService { } }; #[cfg(feature = "web_sys")] - let storage = storage.unwrap().unwrap(); + let storage = storage + .and_then(|storage| storage.ok_or(JsValue::NULL)) + .expect("failed to aquire storage"); StorageService { storage } } diff --git a/src/services/websocket.rs b/src/services/websocket.rs index 7f4e3f5c437..5028a133b24 100644 --- a/src/services/websocket.rs +++ b/src/services/websocket.rs @@ -74,7 +74,7 @@ impl WebSocketService { return Err("Failed to created websocket with given URL"); } - let ws = ws.unwrap(); + let ws = ws.expect("failed to build websocket"); #[cfg(feature = "std_web")] ws.set_binary_type(SocketBinaryType::ArrayBuffer); #[cfg(feature = "web_sys")] From 84064d146f178890cfec792fdb4890ce0a15d8be Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 9 Jan 2020 15:55:52 +0100 Subject: [PATCH 07/11] Use `cfg_if` and `cfg_match` and do some polish. --- src/services/console.rs | 152 ++++++------ src/services/dialog.rs | 48 ++-- src/services/fetch.rs | 498 +++++++++++++++++++------------------- src/services/interval.rs | 32 +-- src/services/keyboard.rs | 48 ++-- src/services/reader.rs | 229 +++++++++--------- src/services/render.rs | 97 ++++---- src/services/resize.rs | 53 ++-- src/services/storage.rs | 85 ++++--- src/services/timeout.rs | 46 ++-- src/services/websocket.rs | 207 ++++++++-------- 11 files changed, 770 insertions(+), 725 deletions(-) diff --git a/src/services/console.rs b/src/services/console.rs index 970b8ddcd28..62ba1252252 100644 --- a/src/services/console.rs +++ b/src/services/console.rs @@ -1,10 +1,16 @@ //! This module contains a service implementation to use browser's console. -#[cfg(feature = "std_web")] -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; -#[cfg(feature = "web_sys")] -use ::{wasm_bindgen::JsValue, web_sys::console}; +use cfg_if::cfg_if; +use cfg_match::cfg_match; +cfg_if! { + if #[cfg(feature = "std_web")] { + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + } else if #[cfg(feature = "web_sys")] { + use wasm_bindgen::JsValue; + use web_sys::console; + } +} /// A service to use methods of a /// [Console](https://developer.mozilla.org/en-US/docs/Web/API/Console). @@ -20,152 +26,152 @@ impl ConsoleService { /// [console.log](https://developer.mozilla.org/en-US/docs/Web/API/Console/log) /// method implementation. pub fn log(&mut self, message: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.log(@{message}); } - #[cfg(feature = "web_sys")] - console::log_1(&JsValue::from_str(message)); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.log(@{message}); }, + feature = "web_sys" => console::log_1(&JsValue::from_str(message)), + }; } /// [console.warn](https://developer.mozilla.org/en-US/docs/Web/API/Console/warn) /// method implementation. pub fn warn(&mut self, message: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.warn(@{message}); } - #[cfg(feature = "web_sys")] - console::warn_1(&JsValue::from_str(message)); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.warn(@{message}); }, + feature = "web_sys" => console::warn_1(&JsValue::from_str(message)), + }; } /// [console.info](https://developer.mozilla.org/en-US/docs/Web/API/Console/info) /// method implementation. pub fn info(&mut self, message: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.info(@{message}); } - #[cfg(feature = "web_sys")] - console::info_1(&JsValue::from_str(message)); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.info(@{message}); }, + feature = "web_sys" => console::info_1(&JsValue::from_str(message)), + }; } /// [console.error](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) /// method implementation. pub fn error(&mut self, message: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.error(@{message}); } - #[cfg(feature = "web_sys")] - console::error_1(&JsValue::from_str(message)); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.error(@{message}); }, + feature = "web_sys" => console::error_1(&JsValue::from_str(message)), + }; } /// [console.debug](https://developer.mozilla.org/en-US/docs/Web/API/Console/debug) /// method implementation. pub fn debug(&mut self, message: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.debug(@{message}); } - #[cfg(feature = "web_sys")] - console::debug_1(&JsValue::from_str(message)); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.debug(@{message}); }, + feature = "web_sys" => console::debug_1(&JsValue::from_str(message)), + }; } /// [console.count_named](https://developer.mozilla.org/en-US/docs/Web/API/Console/count_named) /// method implementation. pub fn count_named(&mut self, name: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.count(@{name}); } - #[cfg(feature = "web_sys")] - console::count_with_label(name); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.count(@{name}); }, + feature = "web_sys" => console::count_with_label(name), + }; } /// [console.count](https://developer.mozilla.org/en-US/docs/Web/API/Console/count) /// method implementation. pub fn count(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.count(); } - #[cfg(feature = "web_sys")] - console::count(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.count(); }, + feature = "web_sys" => console::count(), + }; } /// [console.time_named](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_named) /// method implementation. pub fn time_named(&mut self, name: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.time(@{name}); } - #[cfg(feature = "web_sys")] - console::time_with_label(name); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.time(@{name}); }, + feature = "web_sys" => console::time_with_label(name), + }; } /// [console.time_named_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_named_end) /// method implementation. pub fn time_named_end(&mut self, name: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.timeEnd(@{name}); } - #[cfg(feature = "web_sys")] - console::time_end_with_label(name); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.timeEnd(@{name}); }, + feature = "web_sys" => console::time_end_with_label(name), + }; } /// [console.time](https://developer.mozilla.org/en-US/docs/Web/API/Console/time) /// method implementation. pub fn time(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.time(); } - #[cfg(feature = "web_sys")] - console::time(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.time(); }, + feature = "web_sys" => console::time(), + }; } /// [console.time_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/time_end) /// method implementation. pub fn time_end(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.timeEnd(); } - #[cfg(feature = "web_sys")] - console::time_end(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.timeEnd(); }, + feature = "web_sys" => console::time_end(), + }; } /// [console.clear](https://developer.mozilla.org/en-US/docs/Web/API/Console/clear) /// method implementation. pub fn clear(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.clear(); } - #[cfg(feature = "web_sys")] - console::clear(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.clear(); }, + feature = "web_sys" => console::clear(), + }; } /// [console.group](https://developer.mozilla.org/en-US/docs/Web/API/Console/group) /// method implementation. pub fn group(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.group(); } - #[cfg(feature = "web_sys")] - console::group_0(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.group(); }, + feature = "web_sys" => console::group_0(), + }; } /// [console.group_collapsed](https://developer.mozilla.org/en-US/docs/Web/API/Console/group_collapsed) /// method implementation. pub fn group_collapsed(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.groupCollapsed(); } - #[cfg(feature = "web_sys")] - console::group_collapsed_0(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.groupCollapsed(); }, + feature = "web_sys" => console::group_collapsed_0(), + }; } /// [console.group_end](https://developer.mozilla.org/en-US/docs/Web/API/Console/group_end) /// method implementation. pub fn group_end(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.groupEnd(); } - #[cfg(feature = "web_sys")] - console::group_end(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.groupEnd(); }, + feature = "web_sys" => console::group_end(), + }; } /// [console.trace](https://developer.mozilla.org/en-US/docs/Web/API/Console/trace) /// method implementation. pub fn trace(&mut self) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.trace(); } - #[cfg(feature = "web_sys")] - console::trace_0(); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.trace(); }, + feature = "web_sys" => console::trace_0(), + }; } /// [console.assert](https://developer.mozilla.org/en-US/docs/Web/API/Console/assert) /// method implementation. pub fn assert(&mut self, condition: bool, message: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) console.assert(@{condition}, @{message}); } - #[cfg(feature = "web_sys")] - console::assert_with_condition_and_data_1(condition, &String::from(message).into()); + cfg_match! { + feature = "std_web" => js! { @(no_return) console.assert(@{condition}, @{message}); }, + feature = "web_sys" => console::assert_with_condition_and_data_1(condition, &String::from(message).into()), + }; } } diff --git a/src/services/dialog.rs b/src/services/dialog.rs index da02db41ed6..5bd45a19ce4 100644 --- a/src/services/dialog.rs +++ b/src/services/dialog.rs @@ -1,11 +1,17 @@ //! This module contains the implementation of a service //! to show alerts and confirm dialogs in a browser. -#[cfg(feature = "std_web")] -use stdweb::Value; -#[cfg(feature = "std_web")] -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; +use cfg_if::cfg_if; +use cfg_match::cfg_match; +cfg_if! { + if #[cfg(feature = "std_web")] { + use stdweb::Value; + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + } else if #[cfg(feature = "web_sys")] { + use crate::utils; + } +} /// A dialog service. #[derive(Default, Debug)] @@ -20,30 +26,24 @@ impl DialogService { /// Calls [alert](https://developer.mozilla.org/en-US/docs/Web/API/Window/alert) /// function. pub fn alert(&mut self, message: &str) { - #[cfg(feature = "std_web")] - js! { @(no_return) alert(@{message}); } - #[cfg(feature = "web_sys")] - web_sys::window() - .unwrap() - .alert_with_message(message) - .unwrap(); + cfg_match! { + feature = "std_web" => js! { @(no_return) alert(@{message}); }, + feature = "web_sys" => utils::window().alert_with_message(message).unwrap(), + }; } /// Calls [confirm](https://developer.mozilla.org/en-US/docs/Web/API/Window/confirm) /// function. pub fn confirm(&mut self, message: &str) -> bool { - #[cfg(feature = "std_web")] - { - let value: Value = js! { return confirm(@{message}); }; - match value { - Value::Bool(result) => result, - _ => false, - } + cfg_match! { + feature = "std_web" => ({ + let value: Value = js! { return confirm(@{message}); }; + match value { + Value::Bool(result) => result, + _ => false, + } + }), + feature = "web_sys" => utils::window().confirm_with_message(message).unwrap(), } - #[cfg(feature = "web_sys")] - web_sys::window() - .unwrap() - .confirm_with_message(message) - .unwrap() } } diff --git a/src/services/fetch.rs b/src/services/fetch.rs index dfb94ef5879..6c589addf7c 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -3,42 +3,35 @@ use super::Task; use crate::callback::Callback; use crate::format::{Binary, Format, Text}; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use failure::Fail; use std::fmt; -#[cfg(feature = "std_web")] -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; -#[cfg(feature = "web_sys")] -pub use web_sys::{ - RequestCache as Cache, RequestCredentials as Credentials, RequestMode as Mode, - RequestRedirect as Redirect, -}; -#[cfg(feature = "web_sys")] -use ::{ - js_sys::{Array, Promise, Uint8Array}, - std::{ - rc::Rc, - sync::{ - atomic::{AtomicBool, Ordering}, - mpsc::{self, Receiver}, - }, - }, - wasm_bindgen::{closure::Closure, JsValue}, - web_sys::{ - AbortController, Headers, Request as WebRequest, RequestInit, Response as WebResponse, - }, -}; -#[cfg(feature = "std_web")] -use ::{ - serde::Serialize, - std::collections::HashMap, - stdweb::{ - serde::Serde, - unstable::{TryFrom, TryInto}, - web::ArrayBuffer, - JsSerialize, Value, - }, -}; +cfg_if! { + if #[cfg(feature = "std_web")] { + use serde::Serialize; + use std::collections::HashMap; + use stdweb::serde::Serde; + use stdweb::unstable::{TryFrom, TryInto}; + use stdweb::web::ArrayBuffer; + use stdweb::{JsSerialize, Value}; + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + } else if #[cfg(feature = "web_sys")] { + use js_sys::{Array, Promise, Uint8Array}; + use std::rc::Rc; + use std::sync::atomic::{AtomicBool, Ordering}; + use std::sync::mpsc::{self, Receiver}; + use wasm_bindgen::{closure::Closure, JsValue}; + use web_sys::{ + AbortController, Headers, Request as WebRequest, RequestInit, Response as WebResponse, + }; + pub use web_sys::{ + RequestCache as Cache, RequestCredentials as Credentials, RequestMode as Mode, + RequestRedirect as Redirect, + }; + } +} pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri}; @@ -308,19 +301,19 @@ impl FetchService { IN: Into, OUT: From, { - #[cfg(feature = "std_web")] - { - fetch_impl::(false, request, None, callback) + cfg_match! { + feature = "std_web" => fetch_impl::(false, request, None, callback), + feature = "web_sys" => ({ + fetch_impl::( + false, + request, + None, + callback, + Into::into, + |v| v.as_string().unwrap(), + ) + }), } - #[cfg(feature = "web_sys")] - fetch_impl::( - false, - request, - None, - callback, - Into::into, - |v| v.as_string().unwrap(), - ) } /// `fetch` with provided `FetchOptions` object. @@ -363,19 +356,19 @@ impl FetchService { IN: Into, OUT: From, { - #[cfg(feature = "std_web")] - { - fetch_impl::(false, request, Some(options), callback) + cfg_match! { + feature = "std_web" => fetch_impl::(false, request, Some(options), callback), + feature = "web_sys" => ({ + fetch_impl::( + false, + request, + Some(options), + callback, + Into::into, + |v| v.as_string().unwrap(), + ) + }), } - #[cfg(feature = "web_sys")] - fetch_impl::( - false, - request, - Some(options), - callback, - Into::into, - |v| v.as_string().unwrap(), - ) } /// Fetch the data in binary format. @@ -388,19 +381,19 @@ impl FetchService { IN: Into, OUT: From, { - #[cfg(feature = "std_web")] - { - fetch_impl::, ArrayBuffer>(true, request, None, callback) + cfg_match! { + feature = "std_web" => fetch_impl::, ArrayBuffer>(true, request, None, callback), + feature = "web_sys" => ({ + fetch_impl::, ArrayBuffer, _, _>( + true, + request, + None, + callback, + |v| Uint8Array::from(v.as_slice()).into(), + From::from, + ) + }), } - #[cfg(feature = "web_sys")] - fetch_impl::, ArrayBuffer, _, _>( - true, - request, - None, - callback, - |v| Uint8Array::from(v.as_slice()).into(), - From::from, - ) } /// Fetch the data in binary format. @@ -414,17 +407,19 @@ impl FetchService { IN: Into, OUT: From, { - #[cfg(feature = "std_web")] - return fetch_impl::, ArrayBuffer>(true, request, Some(options), callback); - #[cfg(feature = "web_sys")] - fetch_impl::, ArrayBuffer, _, _>( - true, - request, - Some(options), - callback, - |v| Uint8Array::from(v.as_slice()).into(), - From::from, - ) + cfg_match! { + feature = "std_web" => fetch_impl::, ArrayBuffer>(true, request, Some(options), callback), + feature = "web_sys" => ({ + fetch_impl::, ArrayBuffer, _, _>( + true, + request, + Some(options), + callback, + |v| Uint8Array::from(v.as_slice()).into(), + From::from, + ) + }), + } } } @@ -460,15 +455,15 @@ where .unwrap_or_else(|_| panic!("Unparsable request header {}: {:?}", k.as_str(), v)), ) }); - #[cfg(feature = "std_web")] - let header_map: HashMap<&str, &str> = header_map.collect(); - #[cfg(feature = "web_sys")] - let header_map = { - let headers = Headers::new().unwrap(); - for (k, v) in header_map { - headers.append(k, v).unwrap(); - } - headers + let header_map = cfg_match! { + feature = "std_web" => header_map.collect::>(), + feature = "web_sys" => ({ + let headers = Headers::new().unwrap(); + for (k, v) in header_map { + headers.append(k, v).unwrap(); + } + headers + }), }; // Formats URI. let uri = format!("{}", parts.uri); @@ -485,17 +480,22 @@ where #[cfg(feature = "web_sys")] headers: Headers, #[cfg(feature = "std_web")] data: X| { let mut response_builder = Response::builder().status(status); - #[cfg(feature = "web_sys")] - let headers = js_sys::try_iter(&headers) - .unwrap() - .unwrap() - .map(Result::unwrap) - .map(|entry| { - let entry = Array::from(&entry); - let key = entry.get(0); - let value = entry.get(1); - (key.as_string().unwrap(), value.as_string().unwrap()) - }); + // convert `headers` to `Iterator` + let headers = cfg_match! { + feature = "std_web" => headers.into_iter(), + feature = "web_sys" => ({ + js_sys::try_iter(&headers) + .unwrap() + .unwrap() + .map(Result::unwrap) + .map(|entry| { + let entry = Array::from(&entry); + let key = entry.get(0); + let value = entry.get(1); + (key.as_string().unwrap(), value.as_string().unwrap()) + }) + }), + }; for (key, value) in headers { response_builder = response_builder.header(key.as_str(), value.as_str()); } @@ -513,128 +513,126 @@ where callback.emit(response); }; - #[cfg(feature = "std_web")] #[allow(clippy::too_many_arguments)] - let handle = js! { - var body = @{body}; - if (@{binary} && body != null) { - body = Uint8Array.from(body); - } - var data = { - method: @{method}, - body: body, - headers: @{header_map}, - }; - var request = new Request(@{uri}, data); - var callback = @{callback}; - var abortController = AbortController ? new AbortController() : null; - var handle = { - active: true, - callback, - abortController, - }; - var init = @{Serde(options)} || {}; - if (abortController && !("signal" in init)) { - init.signal = abortController.signal; - } - fetch(request, init).then(function(response) { - var promise = (@{binary}) ? response.arrayBuffer() : response.text(); - var status = response.status; - var headers = {}; - response.headers.forEach(function(value, key) { - headers[key] = value; - }); - promise.then(function(data) { - if (handle.active == true) { - handle.active = false; - callback(true, status, headers, data); - callback.drop(); - } - }).catch(function(err) { + let handle = cfg_match! { + feature = "std_web" => js! { + var body = @{body}; + if (@{binary} && body != null) { + body = Uint8Array.from(body); + } + var data = { + method: @{method}, + body: body, + headers: @{header_map}, + }; + var request = new Request(@{uri}, data); + var callback = @{callback}; + var abortController = AbortController ? new AbortController() : null; + var handle = { + active: true, + callback, + abortController, + }; + var init = @{Serde(options)} || {}; + if (abortController && !("signal" in init)) { + init.signal = abortController.signal; + } + fetch(request, init).then(function(response) { + var promise = (@{binary}) ? response.arrayBuffer() : response.text(); + var status = response.status; + var headers = {}; + response.headers.forEach(function(value, key) { + headers[key] = value; + }); + promise.then(function(data) { + if (handle.active == true) { + handle.active = false; + callback(true, status, headers, data); + callback.drop(); + } + }).catch(function(err) { + if (handle.active == true) { + handle.active = false; + callback(false, status, headers, data); + callback.drop(); + } + }); + }).catch(function(e) { if (handle.active == true) { + var data = (@{binary}) ? new ArrayBuffer() : ""; handle.active = false; - callback(false, status, headers, data); + callback(false, 408, {}, data); callback.drop(); } }); - }).catch(function(e) { - if (handle.active == true) { - var data = (@{binary}) ? new ArrayBuffer() : ""; - handle.active = false; - callback(false, 408, {}, data); - callback.drop(); - } - }); - return handle; - }; - #[cfg(feature = "web_sys")] - let handle = { - let mut data = RequestInit::new(); - data.method(method); - data.body(body.map(into_conversion).as_ref()); - data.headers(&header_map); - let request = WebRequest::new_with_str_and_init(&uri, &data).unwrap(); - let active = Rc::new(AtomicBool::new(true)); - let (sender, receiver) = mpsc::channel(); - let active_outer_clone = Rc::clone(&active); - let callback_outer_clone = callback.clone(); - let sender_clone = sender.clone(); - let closure_then = move |response: JsValue| { - let response = WebResponse::from(response); - let promise = if binary { - response.array_buffer() - } else { - response.text() - } - .unwrap(); - let status = response.status(); - let headers = response.headers(); - let active_clone = Rc::clone(&active_outer_clone); - let callback_clone = callback_outer_clone.clone(); - let headers_clone = headers.clone(); - let closure_then = move |data: JsValue| { - let data = from_conversion(data); - if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { - callback_clone(Some(data), status, headers_clone); + return handle; + }, + feature = "web_sys" => ({ + let mut data = RequestInit::new(); + data.method(method); + data.body(body.map(into_conversion).as_ref()); + data.headers(&header_map); + let request = WebRequest::new_with_str_and_init(&uri, &data).unwrap(); + let active = Rc::new(AtomicBool::new(true)); + let (sender, receiver) = mpsc::channel(); + let active_outer_clone = Rc::clone(&active); + let callback_outer_clone = callback.clone(); + let sender_clone = sender.clone(); + let closure_then = move |response: JsValue| { + let response = WebResponse::from(response); + let promise = if binary { + response.array_buffer() + } else { + response.text() } + .unwrap(); + let status = response.status(); + let headers = response.headers(); + let active_clone = Rc::clone(&active_outer_clone); + let callback_clone = callback_outer_clone.clone(); + let headers_clone = headers.clone(); + let closure_then = move |data: JsValue| { + let data = from_conversion(data); + if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { + callback_clone(Some(data), status, headers_clone); + } + }; + let closure_then = Closure::once(closure_then); + let closure_catch = move |_| { + if active_outer_clone.compare_and_swap(true, false, Ordering::SeqCst) { + callback_outer_clone(None, status, headers); + } + }; + let closure_catch = Closure::once(closure_catch); + promise.then(&closure_then).catch(&closure_catch); + sender_clone.send(closure_then).unwrap(); + sender_clone.send(closure_catch).unwrap(); }; let closure_then = Closure::once(closure_then); + let active_clone = Rc::clone(&active); let closure_catch = move |_| { - if active_outer_clone.compare_and_swap(true, false, Ordering::SeqCst) { - callback_outer_clone(None, status, headers); + if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { + callback(None, 408, Headers::new().unwrap()); } }; - let closure_catch = Closure::once(closure_catch); - promise.then(&closure_then).catch(&closure_catch); - sender_clone.send(closure_then).unwrap(); - sender_clone.send(closure_catch).unwrap(); - }; - let closure_then = Closure::once(closure_then); - let active_clone = Rc::clone(&active); - let closure_catch = move |_| { - if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { - callback(None, 408, Headers::new().unwrap()); + let closure_catch = Closure::wrap(Box::new(closure_catch) as Box); + let abort_controller = AbortController::new().ok(); + let mut init = options.map_or_else(RequestInit::new, Into::into); + if let Some(abort_controller) = &abort_controller { + init.signal(Some(&abort_controller.signal())); } - }; - let closure_catch = Closure::wrap(Box::new(closure_catch) as Box); - let abort_controller = AbortController::new().ok(); - let mut init = options.map_or_else(RequestInit::new, Into::into); - if let Some(abort_controller) = &abort_controller { - init.signal(Some(&abort_controller.signal())); - } - let promise = web_sys::window() - .unwrap() - .fetch_with_request_and_init(&request, &init) - .then(&closure_then) - .catch(&closure_catch); - sender.send(closure_then).unwrap(); - sender.send(closure_catch).unwrap(); - Handle { - active, - callbacks: receiver, - abort_controller, - promise, - } + let promise = global!(global, global.fetch_with_request_and_init(&request, &init)) + .then(&closure_then) + .catch(&closure_catch); + sender.send(closure_then).unwrap(); + sender.send(closure_catch).unwrap(); + Handle { + active, + callbacks: receiver, + abort_controller, + promise, + } + }), }; FetchTask(Some(handle)) } @@ -642,24 +640,24 @@ where impl Task for FetchTask { fn is_active(&self) -> bool { if let Some(ref task) = self.0 { - #[cfg(feature = "std_web")] - { - let result = js! { - var the_task = @{task}; - return the_task.active && - (!the_task.abortController || !the_task.abortController.signal.aborted); - }; - result.try_into().unwrap_or(false) - } - #[cfg(feature = "web_sys")] - { - task.active.load(Ordering::SeqCst) - && task - .abort_controller - .as_ref() - .map(|abort_controller| abort_controller.signal().aborted()) - .filter(|value| *value) - .is_none() + cfg_match! { + feature = "std_web" => ({ + let result = js! { + var the_task = @{task}; + return the_task.active && + (!the_task.abortController || !the_task.abortController.signal.aborted); + }; + result.try_into().unwrap_or(false) + }), + feature = "web_sys" => ({ + task.active.load(Ordering::SeqCst) + && task + .abort_controller + .as_ref() + .map(|abort_controller| abort_controller.signal().aborted()) + .filter(|value| *value) + .is_none() + }), } } else { false @@ -673,27 +671,29 @@ impl Task for FetchTask { .0 .take() .expect("tried to cancel request fetching twice"); - #[cfg(feature = "std_web")] - js! { @(no_return) - var handle = @{handle}; - handle.active = false; - handle.callback.drop(); - if (handle.abortController) { - handle.abortController.abort(); - } - } - #[cfg(feature = "web_sys")] - { - thread_local! { - static CATCH: Closure = Closure::wrap(Box::new(|_| ()) as Box); - } - handle.active.store(false, Ordering::SeqCst); - CATCH.with(|c| handle.promise.catch(&c)); - if let Some(abort_controller) = handle.abort_controller { - abort_controller.abort(); - } - handle.callbacks.try_iter().for_each(drop); - } + cfg_match! { + feature = "std_web" => ({ + js! { @(no_return) + var handle = @{handle}; + handle.active = false; + handle.callback.drop(); + if (handle.abortController) { + handle.abortController.abort(); + } + }; + }), + feature = "web_sys" => ({ + thread_local! { + static CATCH: Closure = Closure::wrap(Box::new(|_| ()) as Box); + } + handle.active.store(false, Ordering::SeqCst); + CATCH.with(|c| handle.promise.catch(&c)); + if let Some(abort_controller) = handle.abort_controller { + abort_controller.abort(); + } + handle.callbacks.try_iter().for_each(drop); + }), + }; } } diff --git a/src/services/interval.rs b/src/services/interval.rs index 18a5eed9f33..c29b0819800 100644 --- a/src/services/interval.rs +++ b/src/services/interval.rs @@ -3,15 +3,19 @@ use super::{to_ms, Task}; use crate::callback::Callback; -#[cfg(feature = "web_sys")] -use gloo::timers::callback::Interval; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use std::fmt; use std::time::Duration; -#[cfg(feature = "std_web")] -use stdweb::Value; -#[cfg(feature = "std_web")] -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; +cfg_if! { + if #[cfg(feature = "std_web")] { + use stdweb::Value; + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + } else if #[cfg(feature = "web_sys")] { + use gloo::timers::callback::Interval; + } +} /// A handle which helps to cancel interval. Uses /// [clearInterval](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/clearInterval). @@ -44,9 +48,8 @@ impl IntervalService { callback.emit(()); }; let ms = to_ms(duration); - #[cfg(feature = "std_web")] - { - let handle = js! { + let handle = cfg_match! { + feature = "std_web" => js! { var callback = @{callback}; var action = function() { callback(); @@ -56,11 +59,10 @@ impl IntervalService { interval_id: setInterval(action, delay), callback: callback, }; - }; - IntervalTask(Some(handle)) - } - #[cfg(feature = "web_sys")] - IntervalTask(Some(Interval::new(ms, callback))) + }, + feature = "web_sys" => Interval::new(ms, callback), + }; + IntervalTask(Some(handle)) } } diff --git a/src/services/keyboard.rs b/src/services/keyboard.rs index 02d923e7c42..9741137e0b4 100644 --- a/src/services/keyboard.rs +++ b/src/services/keyboard.rs @@ -1,17 +1,19 @@ //! Service to register key press event listeners on elements. + use crate::callback::Callback; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use std::fmt; -#[cfg(feature = "std_web")] -use stdweb::web::{ - event::{ConcreteEvent, KeyDownEvent, KeyPressEvent, KeyUpEvent}, - EventListenerHandle, IEventTarget, -}; -#[cfg(feature = "web_sys")] -use ::{ - gloo::events::EventListener, - wasm_bindgen::JsCast, - web_sys::{Event, EventTarget, KeyboardEvent}, -}; +cfg_if! { + if #[cfg(feature = "std_web")] { + use stdweb::web::event::{ConcreteEvent, KeyDownEvent, KeyPressEvent, KeyUpEvent}; + use stdweb::web::{EventListenerHandle, IEventTarget}; + } else if #[cfg(feature = "web_sys")] { + use gloo::events::EventListener; + use wasm_bindgen::JsCast; + use web_sys::{Event, EventTarget, KeyboardEvent}; + } +} /// Service for registering callbacks on elements to get keystrokes from the user. /// @@ -56,12 +58,10 @@ impl KeyboardService { #[cfg(feature = "std_web")] callback: Callback, #[cfg(feature = "web_sys")] callback: Callback, ) -> KeyListenerHandle { - #[cfg(feature = "std_web")] - { - register_key_impl(element, callback) + cfg_match! { + feature = "std_web" => register_key_impl(element, callback), + feature = "web_sys" => register_key_impl(element, callback, "keypress"), } - #[cfg(feature = "web_sys")] - register_key_impl(element, callback, "keypress") } /// Registers a callback that listens to KeyDownEvents on a provided element. @@ -81,10 +81,10 @@ impl KeyboardService { #[cfg(feature = "std_web")] callback: Callback, #[cfg(feature = "web_sys")] callback: Callback, ) -> KeyListenerHandle { - #[cfg(feature = "std_web")] - return register_key_impl(element, callback); - #[cfg(feature = "web_sys")] - register_key_impl(element, callback, "keydown") + cfg_match! { + feature = "std_web" => register_key_impl(element, callback), + feature = "web_sys" => register_key_impl(element, callback, "keydown"), + } } /// Registers a callback that listens to KeyUpEvents on a provided element. @@ -104,10 +104,10 @@ impl KeyboardService { #[cfg(feature = "std_web")] callback: Callback, #[cfg(feature = "web_sys")] callback: Callback, ) -> KeyListenerHandle { - #[cfg(feature = "std_web")] - return register_key_impl(element, callback); - #[cfg(feature = "web_sys")] - register_key_impl(element, callback, "keyup") + cfg_match! { + feature = "std_web" => register_key_impl(element, callback), + feature = "web_sys" => register_key_impl(element, callback, "keyup"), + } } } diff --git a/src/services/reader.rs b/src/services/reader.rs index 7b3fc8f99c0..20fb55b9752 100644 --- a/src/services/reader.rs +++ b/src/services/reader.rs @@ -2,29 +2,25 @@ use super::Task; use crate::callback::Callback; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use std::cmp; use std::fmt; -#[cfg(feature = "std_web")] -pub use stdweb::web::{Blob, File, IBlob}; -#[cfg(feature = "std_web")] -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; -#[cfg(feature = "std_web")] -use stdweb::{ - unstable::TryInto, - web::{ - event::LoadEndEvent, FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, - TypedArray, - }, -}; -#[cfg(feature = "web_sys")] -pub use web_sys::{Blob, File}; -#[cfg(feature = "web_sys")] -use ::{ - gloo::events::EventListener, - js_sys::Uint8Array, - web_sys::{Event, FileReader}, -}; +cfg_if! { + if #[cfg(feature = "std_web")] { + pub use stdweb::web::{Blob, File, IBlob}; + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + use stdweb::unstable::TryInto; + use stdweb::web::event::LoadEndEvent; + use stdweb::web::{FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, TypedArray}; + } else if #[cfg(feature = "web_sys")] { + pub use web_sys::{Blob, File}; + use gloo::events::EventListener; + use js_sys::Uint8Array; + use web_sys::{Event, FileReader}; + } +} /// Struct that represents data of a file. #[derive(Clone, Debug)] @@ -73,35 +69,43 @@ impl ReaderService { let name = file.name(); let callback = move |#[cfg(feature = "std_web")] _event: LoadEndEvent, #[cfg(feature = "web_sys")] _event: &Event| { - #[cfg(feature = "std_web")] - match reader.result() { - Some(FileReaderResult::String(_)) => { - unreachable!(); - } - Some(FileReaderResult::ArrayBuffer(buffer)) => { - let array: TypedArray = buffer.into(); + cfg_match! { + feature = "std_web" => ({ + match reader.result() { + Some(FileReaderResult::String(_)) => { + unreachable!(); + } + Some(FileReaderResult::ArrayBuffer(buffer)) => { + let array: TypedArray = buffer.into(); + let data = FileData { + name: name.clone(), + content: array.to_vec(), + }; + callback.emit(data); + } + None => {} + } + }), + feature = "web_sys" => ({ + let array = Uint8Array::new_with_byte_offset( + &reader + .result() + .expect("`FileReader` hasn't finished loading"), + 0, + ); let data = FileData { name: name.clone(), content: array.to_vec(), }; callback.emit(data); - } - None => {} - } - #[cfg(feature = "web_sys")] - { - let array = Uint8Array::new_with_byte_offset(&reader.result().unwrap(), 0); - let data = FileData { - name: name.clone(), - content: array.to_vec(), - }; - callback.emit(data); - } + }), + }; + }; + #[cfg_attr(feature = "std_web", allow(unused_variables))] + let listener = cfg_match! { + feature = "std_web" => file_reader.add_event_listener(callback), + feature = "web_sys" => EventListener::new(&file_reader, "loadend", callback), }; - #[cfg(feature = "std_web")] - file_reader.add_event_listener(callback); - #[cfg(feature = "web_sys")] - let listener = EventListener::new(&file_reader, "loadend", callback); file_reader.read_as_array_buffer(&file).unwrap(); ReaderTask { file_reader, @@ -122,82 +126,89 @@ impl ReaderService { let file_reader = file_reader.unwrap(); let name = file.name(); let mut position = 0; - #[cfg(feature = "std_web")] - let total_size = file.len() as usize; - #[cfg(feature = "web_sys")] - let total_size = file.size() as usize; + let total_size = cfg_match! { + feature = "std_web" => file.len(), + feature = "web_sys" => file.size(), + } as usize; let reader = file_reader.clone(); let callback = move |#[cfg(feature = "std_web")] _event: LoadEndEvent, #[cfg(feature = "web_sys")] _event: &Event| { - #[cfg(feature = "std_web")] - match reader.result() { - // This branch is used to start reading - Some(FileReaderResult::String(_)) => { - let started = FileChunk::Started { name: name.clone() }; - callback.emit(started); - } - // This branch is used to send a chunk value - Some(FileReaderResult::ArrayBuffer(buffer)) => { - let array: TypedArray = buffer.into(); - let chunk = FileChunk::DataChunk { - data: array.to_vec(), - progress: position as f32 / total_size as f32, - }; - callback.emit(chunk); - } - None => {} - } - #[cfg(feature = "web_sys")] - { - let result = reader.result().unwrap(); + cfg_match! { + feature = "std_web" => ({ + match reader.result() { + // This branch is used to start reading + Some(FileReaderResult::String(_)) => { + let started = FileChunk::Started { name: name.clone() }; + callback.emit(started); + } + // This branch is used to send a chunk value + Some(FileReaderResult::ArrayBuffer(buffer)) => { + let array: TypedArray = buffer.into(); + let chunk = FileChunk::DataChunk { + data: array.to_vec(), + progress: position as f32 / total_size as f32, + }; + callback.emit(chunk); + } + None => {} + } + }), + feature = "web_sys" => ({ + let result = reader + .result() + .expect("`FileReader` hasn't finished loading"); - if result.is_string() { - let started = FileChunk::Started { name: name.clone() }; - callback.emit(started); - } else { - let array = Uint8Array::new_with_byte_offset(&result, 0); - let chunk = FileChunk::DataChunk { - data: array.to_vec(), - progress: position as f32 / total_size as f32, - }; - callback.emit(chunk); - } - } + if result.is_string() { + let started = FileChunk::Started { name: name.clone() }; + callback.emit(started); + } else { + let array = Uint8Array::new_with_byte_offset(&result, 0); + let chunk = FileChunk::DataChunk { + data: array.to_vec(), + progress: position as f32 / total_size as f32, + }; + callback.emit(chunk); + } + }), + }; // Read the next chunk if position < total_size { let from = position; let to = cmp::min(position + chunk_size, total_size); position = to; - #[cfg(feature = "std_web")] - // TODO Implement `slice` method in `stdweb` - let blob: Blob = { - let file = &file; - (js! { - return @{file}.slice(@{from as u32}, @{to as u32}); - }) - .try_into() - .unwrap() + let blob: Blob = cfg_match! { + feature = "std_web" => ({ + // TODO Implement `slice` method in `stdweb` + let file = &file; + (js! { + return @{file}.slice(@{from as u32}, @{to as u32}); + }) + .try_into() + .unwrap() + }), + feature = "web_sys" => file.slice_with_i32_and_i32(from as _, to as _).unwrap(), }; - #[cfg(feature = "web_sys")] - let blob = file.slice_with_i32_and_i32(from as _, to as _).unwrap(); reader.read_as_array_buffer(&blob).unwrap(); } else { let finished = FileChunk::Finished; callback.emit(finished); } }; - #[cfg(feature = "std_web")] - file_reader.add_event_listener(callback); - #[cfg(feature = "web_sys")] - let listener = EventListener::new(&file_reader, "loadend", callback); - #[cfg(feature = "std_web")] - let blob: Blob = (js! { - return (new Blob()); - }) - .try_into() - .unwrap(); - #[cfg(feature = "web_sys")] - let blob = Blob::new().unwrap(); + #[cfg_attr(feature = "std_web", allow(unused_variables))] + let listener = cfg_match! { + feature = "std_web" => file_reader.add_event_listener(callback), + feature = "web_sys" => EventListener::new(&file_reader, "loadend", callback), + }; + let blob: Blob = cfg_match! { + feature = "std_web" => ({ + (js! { + return (new Blob()); + }) + .try_into() + .unwrap() + }), + feature = "web_sys" => Blob::new().unwrap(), + }; file_reader.read_as_text(&blob).unwrap(); ReaderTask { file_reader, @@ -224,13 +235,9 @@ impl fmt::Debug for ReaderTask { impl Task for ReaderTask { fn is_active(&self) -> bool { - #[cfg(feature = "std_web")] - { - self.file_reader.ready_state() == FileReaderReadyState::Loading - } - #[cfg(feature = "web_sys")] - { - self.file_reader.ready_state() == FileReader::LOADING + cfg_match! { + feature = "std_web" => self.file_reader.ready_state() == FileReaderReadyState::Loading, + feature = "web_sys" => self.file_reader.ready_state() == FileReader::LOADING, } } diff --git a/src/services/render.rs b/src/services/render.rs index 6f3bead4fe6..cccb62afb70 100644 --- a/src/services/render.rs +++ b/src/services/render.rs @@ -3,14 +3,21 @@ use crate::callback::Callback; use crate::services::Task; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use std::fmt; -#[cfg(feature = "std_web")] -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; -#[cfg(feature = "std_web")] -use stdweb::{unstable::TryInto, Value}; -#[cfg(feature = "web_sys")] -use wasm_bindgen::{closure::Closure, JsCast, JsValue}; +cfg_if! { + if #[cfg(feature = "std_web")] { + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + use stdweb::unstable::TryInto; + use stdweb::Value; + } else if #[cfg(feature = "web_sys")] { + use crate::utils; + use wasm_bindgen::closure::Closure; + use wasm_bindgen::{JsCast, JsValue}; + } +} /// A handle to cancel a render task. #[must_use] @@ -46,38 +53,37 @@ impl RenderService { pub fn request_animation_frame(&mut self, callback: Callback) -> RenderTask { let callback = move |#[cfg(feature = "std_web")] v, #[cfg(feature = "web_sys")] v: JsValue| { - #[cfg(feature = "std_web")] - let time: f64 = match v { - Value::Number(n) => n.try_into().unwrap(), - _ => 0.0, + let time: f64 = cfg_match! { + feature = "std_web" => ({ + match v { + Value::Number(n) => n.try_into().unwrap(), + _ => 0.0, + } + }), + feature = "web_sys" => v.as_f64().unwrap_or(0.), }; - #[cfg(feature = "web_sys")] - let time = v.as_f64().unwrap_or(0.); callback.emit(time); }; - #[cfg(feature = "std_web")] - let handle = js! { - var callback = @{callback}; - var action = function(time) { - callback(time); - callback.drop(); - }; - return { - render_id: requestAnimationFrame(action), - callback: callback, - }; - }; - #[cfg(feature = "web_sys")] - let handle = { - let callback = Closure::wrap(Box::new(callback) as Box); - let render_id = web_sys::window() - .unwrap() - .request_animation_frame(callback.as_ref().unchecked_ref()) - .unwrap(); - RenderTaskInner { - render_id, - callback, - } + let handle = cfg_match! { + feature = "std_web" => js! { + var callback = @{callback}; + var action = function(time) { + callback(time); + callback.drop(); + }; + return { + render_id: requestAnimationFrame(action), + callback: callback, + }; + }, + feature = "web_sys" => ({ + let callback = Closure::wrap(Box::new(callback) as Box); + let render_id = utils::window().request_animation_frame(callback.as_ref().unchecked_ref()).unwrap(); + RenderTaskInner { + render_id, + callback, + } + }), }; RenderTask(Some(handle)) } @@ -89,17 +95,14 @@ impl Task for RenderTask { } fn cancel(&mut self) { let handle = self.0.take().expect("tried to cancel render twice"); - #[cfg(feature = "std_web")] - js! { @(no_return) - var handle = @{handle}; - cancelAnimationFrame(handle.render_id); - handle.callback.drop(); - } - #[cfg(feature = "web_sys")] - web_sys::window() - .unwrap() - .cancel_animation_frame(handle.render_id) - .unwrap(); + cfg_match! { + feature = "std_web" => js! { @(no_return) + var handle = @{handle}; + cancelAnimationFrame(handle.render_id); + handle.callback.drop(); + }, + feature = "web_sys" => utils::window().cancel_animation_frame(handle.render_id).unwrap(), + }; } } diff --git a/src/services/resize.rs b/src/services/resize.rs index 3b968a8fe5c..1ed92343e19 100644 --- a/src/services/resize.rs +++ b/src/services/resize.rs @@ -1,17 +1,18 @@ //! This module contains the implementation of a service that listens for browser window resize events. +use cfg_if::cfg_if; +use cfg_match::cfg_match; use std::fmt; -#[cfg(feature = "std_web")] -use stdweb::{ - js, - web::{window, Window}, - Value, -}; use yew::callback::Callback; -#[cfg(feature = "web_sys")] -use ::{ - gloo::events::EventListener, - web_sys::{Event, Window}, -}; +cfg_if! { + if #[cfg(feature = "std_web")] { + use stdweb::js; + use stdweb::web::{window, Window}; + use stdweb::Value; + } else if #[cfg(feature = "web_sys")] { + use gloo::events::EventListener; + use web_sys::{Event, Window}; + } +} /// A service that fires events when the browser window resizes. #[derive(Default, Debug)] @@ -64,23 +65,25 @@ impl ResizeService { /// Register a callback that will be called when the browser window resizes. pub fn register(&mut self, callback: Callback) -> ResizeTask { let callback = move |#[cfg(feature = "web_sys")] _event: &Event| { - #[cfg(feature = "std_web")] - let window = window(); - #[cfg(feature = "web_sys")] - let window = web_sys::window().unwrap(); + let window = cfg_match! { + feature = "std_web" => window(), + feature = "web_sys" => web_sys::window().unwrap(), + }; let dimensions = WindowDimensions::get_dimensions(&window); callback.emit(dimensions); }; - #[cfg(feature = "std_web")] - let handle = Some(js! { - var callback = @{callback}; - var action = function() { - callback(); - }; - return window.addEventListener("resize", action); - }); - #[cfg(feature = "web_sys")] - let handle = EventListener::new(&web_sys::window().unwrap(), "resize", callback); + let handle = cfg_match! { + feature = "std_web" => ({ + Some(js! { + var callback = @{callback}; + var action = function() { + callback(); + }; + return window.addEventListener("resize", action); + }) + }), + feature = "web_sys" => EventListener::new(&web_sys::window().unwrap(), "resize", callback), + }; ResizeTask(handle) } } diff --git a/src/services/storage.rs b/src/services/storage.rs index a2fcc684907..ffbbdbe8738 100644 --- a/src/services/storage.rs +++ b/src/services/storage.rs @@ -2,12 +2,21 @@ //! use local and session storage of a browser. use crate::format::Text; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use failure::Fail; use std::fmt; -#[cfg(feature = "std_web")] -use stdweb::web::{window, Storage}; -#[cfg(feature = "web_sys")] -use ::{wasm_bindgen::JsValue, web_sys::Storage}; +cfg_if! { + if #[cfg(feature = "std_web")] { + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + use stdweb::unstable::TryFrom; + use stdweb::web::{Storage}; + } else if #[cfg(feature = "web_sys")] { + use crate::utils; + use web_sys::Storage; + } +} /// Represents errors of a storage. #[derive(Debug, Fail)] @@ -38,22 +47,36 @@ impl fmt::Debug for StorageService { impl StorageService { /// Creates a new storage service instance with specified storage area. - pub fn new(area: Area) -> Self { - let storage = { - #[cfg(feature = "std_web")] - let window = window(); - #[cfg(feature = "web_sys")] - let window = web_sys::window().unwrap(); - match area { - Area::Local => window.local_storage(), - Area::Session => window.session_storage(), - } + pub fn new(area: Area) -> Result { + let storage = cfg_match! { + feature = "std_web" => ({ + let storage_name = match area { + Area::Local => "localStorage", + Area::Session => "sessionStorage", + }; + let storage = js! { + try { + return window[@{storage_name}]; + } catch(error) { + return error; + } + }; + Storage::try_from(js!( return @{storage.as_ref()}; )) + }), + feature = "web_sys" => ({ + let storage = { + match area { + Area::Local => utils::window().local_storage(), + Area::Session => utils::window().session_storage(), + } + }; + storage.map(Option::unwrap) + }), }; - #[cfg(feature = "web_sys")] - let storage = storage - .and_then(|storage| storage.ok_or(JsValue::NULL)) - .expect("failed to aquire storage"); - StorageService { storage } + + storage + .map(|storage| StorageService { storage }) + .map_err(|_| "couldn't aquire storage") } /// Stores value to the storage. @@ -62,10 +85,10 @@ impl StorageService { T: Into, { if let Ok(data) = value.into() { - #[cfg(feature = "std_web")] - let result = self.storage.insert(key, &data); - #[cfg(feature = "web_sys")] - let result = self.storage.set_item(key, &data); + let result = cfg_match! { + feature = "std_web" => self.storage.insert(key, &data), + feature = "web_sys" => self.storage.set_item(key, &data), + }; result.expect("can't insert value to a storage"); } } @@ -75,19 +98,19 @@ impl StorageService { where T: From, { - #[cfg(feature = "std_web")] - let data = self.storage.get(key); - #[cfg(feature = "web_sys")] - let data = self.storage.get_item(key).unwrap(); + let data = cfg_match! { + feature = "std_web" => self.storage.get(key), + feature = "web_sys" => self.storage.get_item(key).unwrap(), + }; let data = data.ok_or_else(|| StorageError::CantRestore.into()); T::from(data) } /// Removes value from the storage. pub fn remove(&mut self, key: &str) { - #[cfg(feature = "std_web")] - self.storage.remove(key); - #[cfg(feature = "web_sys")] - self.storage.remove_item(key).unwrap(); + cfg_match! { + feature = "std_web" => self.storage.remove(key), + feature = "web_sys" => self.storage.remove_item(key).unwrap(), + }; } } diff --git a/src/services/timeout.rs b/src/services/timeout.rs index 1e5440d0cc9..b2dd195647b 100644 --- a/src/services/timeout.rs +++ b/src/services/timeout.rs @@ -3,15 +3,19 @@ use super::{to_ms, Task}; use crate::callback::Callback; -#[cfg(feature = "web_sys")] -use gloo::timers::callback::Timeout; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use std::fmt; use std::time::Duration; -#[cfg(feature = "std_web")] -use stdweb::Value; -#[cfg(feature = "std_web")] -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; +cfg_if! { + if #[cfg(feature = "std_web")] { + use stdweb::Value; + #[allow(unused_imports)] + use stdweb::{_js_impl, js}; + } else if #[cfg(feature = "web_sys")] { + use gloo::timers::callback::Timeout; + } +} /// A handle to cancel a timeout task. #[must_use] @@ -42,21 +46,21 @@ impl TimeoutService { callback.emit(()); }; let ms = to_ms(duration); - #[cfg(feature = "std_web")] - let handle = js! { - var callback = @{callback}; - var action = function() { - callback(); - callback.drop(); - }; - var delay = @{ms}; - return { - timeout_id: setTimeout(action, delay), - callback: callback, - }; + let handle = cfg_match! { + feature = "std_web" => js! { + var callback = @{callback}; + var action = function() { + callback(); + callback.drop(); + }; + var delay = @{ms}; + return { + timeout_id: setTimeout(action, delay), + callback: callback, + }; + }, + feature = "web_sys" => Timeout::new(ms, callback), }; - #[cfg(feature = "web_sys")] - let handle = Timeout::new(ms, callback); TimeoutTask(Some(handle)) } } diff --git a/src/services/websocket.rs b/src/services/websocket.rs index 5028a133b24..d40ff298164 100644 --- a/src/services/websocket.rs +++ b/src/services/websocket.rs @@ -4,22 +4,21 @@ use super::Task; use crate::callback::Callback; use crate::format::{Binary, Text}; +use cfg_if::cfg_if; +use cfg_match::cfg_match; use std::fmt; -#[cfg(feature = "std_web")] -use stdweb::{ - traits::IMessageEvent, - web::{ - event::{SocketCloseEvent, SocketErrorEvent, SocketMessageEvent, SocketOpenEvent}, - IEventTarget, SocketBinaryType, SocketReadyState, WebSocket, - }, -}; -#[cfg(feature = "web_sys")] -use ::{ - gloo::events::EventListener, - js_sys::Uint8Array, - wasm_bindgen::JsCast, - web_sys::{BinaryType, Event, MessageEvent, WebSocket}, -}; +cfg_if! { + if #[cfg(feature = "std_web")] { + use stdweb::traits::IMessageEvent; + use stdweb::web::event::{SocketCloseEvent, SocketErrorEvent, SocketMessageEvent, SocketOpenEvent}; + use stdweb::web::{IEventTarget, SocketBinaryType, SocketReadyState, WebSocket}; + } else if #[cfg(feature = "web_sys")] { + use gloo::events::EventListener; + use js_sys::Uint8Array; + use wasm_bindgen::JsCast; + use web_sys::{BinaryType, Event, MessageEvent, WebSocket}; + } +} /// A status of a websocket connection. Used for status notification. #[derive(Debug)] @@ -74,80 +73,82 @@ impl WebSocketService { return Err("Failed to created websocket with given URL"); } - let ws = ws.expect("failed to build websocket"); - #[cfg(feature = "std_web")] - ws.set_binary_type(SocketBinaryType::ArrayBuffer); - #[cfg(feature = "web_sys")] - ws.set_binary_type(BinaryType::Arraybuffer); - let notify = notification.clone(); - let listener = move |#[cfg(feature = "std_web")] _: SocketOpenEvent, - #[cfg(feature = "web_sys")] _: &Event| { - notify.emit(WebSocketStatus::Opened); + let ws = ws.map_err(|_| "failed to build websocket")?; + cfg_match! { + feature = "std_web" => ws.set_binary_type(SocketBinaryType::ArrayBuffer), + feature = "web_sys" => ws.set_binary_type(BinaryType::Arraybuffer), }; - #[cfg(feature = "std_web")] - ws.add_event_listener(listener); - #[cfg(feature = "web_sys")] - let listener_open = EventListener::new(&ws, "open", listener); let notify = notification.clone(); - let listener = move |#[cfg(feature = "std_web")] _: SocketCloseEvent, - #[cfg(feature = "web_sys")] _: &Event| { - notify.emit(WebSocketStatus::Closed); - }; - #[cfg(feature = "std_web")] - ws.add_event_listener(listener); - #[cfg(feature = "web_sys")] - let listener_close = EventListener::new(&ws, "close", listener); + let listener_open = + move |#[cfg(feature = "std_web")] _: SocketOpenEvent, + #[cfg(feature = "web_sys")] _: &Event| { + notify.emit(WebSocketStatus::Opened); + }; let notify = notification.clone(); - let listener = move |#[cfg(feature = "std_web")] _: SocketErrorEvent, - #[cfg(feature = "web_sys")] _: &Event| { - notify.emit(WebSocketStatus::Error); - }; - #[cfg(feature = "std_web")] - ws.add_event_listener(listener); - #[cfg(feature = "web_sys")] - let listener_error = EventListener::new(&ws, "error", listener); - let listener = move |#[cfg(feature = "std_web")] event: SocketMessageEvent, - #[cfg(feature = "web_sys")] event: &Event| { - #[cfg(feature = "web_sys")] - let data = event.dyn_ref::().unwrap().data(); - #[cfg(feature = "std_web")] - let text = event.data().into_text(); - #[cfg(feature = "web_sys")] - let text = data.as_string(); - #[cfg(feature = "std_web")] - let bytes = event.data().into_array_buffer(); - #[cfg(feature = "web_sys")] - let bytes = Some(data); - - if let Some(text) = text { - let data = Ok(text); - let out = OUT::from(data); - callback.emit(out); - } else if let Some(bytes) = bytes { + let listener_close = + move |#[cfg(feature = "std_web")] _: SocketCloseEvent, + #[cfg(feature = "web_sys")] _: &Event| { + notify.emit(WebSocketStatus::Closed); + }; + let notify = notification.clone(); + let listener_error = + move |#[cfg(feature = "std_web")] _: SocketErrorEvent, + #[cfg(feature = "web_sys")] _: &Event| { + notify.emit(WebSocketStatus::Error); + }; + let listener_message = + move |#[cfg(feature = "std_web")] event: SocketMessageEvent, + #[cfg(feature = "web_sys")] event: &Event| { + #[cfg(feature = "web_sys")] + let data = event.dyn_ref::().unwrap().data(); #[cfg(feature = "std_web")] - let bytes: Vec = bytes.into(); + let text = event.data().into_text(); #[cfg(feature = "web_sys")] - let bytes = Uint8Array::new_with_byte_offset(&bytes, 0).to_vec(); - let data = Ok(bytes); - let out = OUT::from(data); - callback.emit(out); - } - }; - #[cfg(feature = "std_web")] - ws.add_event_listener(listener); - #[cfg(feature = "web_sys")] - let listener_message = EventListener::new(&ws, "message", listener); - Ok(WebSocketTask { - ws, - notification, - #[cfg(feature = "web_sys")] - listeners: [ - listener_open, - listener_close, - listener_error, - listener_message, - ], - }) + let text = data.as_string(); + #[cfg(feature = "std_web")] + let bytes = event.data().into_array_buffer(); + #[cfg(feature = "web_sys")] + let bytes = Some(data); + + if let Some(text) = text { + let data = Ok(text); + let out = OUT::from(data); + callback.emit(out); + } else if let Some(bytes) = bytes { + #[cfg(feature = "std_web")] + let bytes: Vec = bytes.into(); + #[cfg(feature = "web_sys")] + let bytes = Uint8Array::new_with_byte_offset(&bytes, 0).to_vec(); + let data = Ok(bytes); + let out = OUT::from(data); + callback.emit(out); + } + }; + #[cfg_attr(feature = "std_web", allow(clippy::let_unit_value, unused_variables))] + { + let listeners = cfg_match! { + feature = "std_web" => ({ + ws.add_event_listener(listener_open); + ws.add_event_listener(listener_close); + ws.add_event_listener(listener_error); + ws.add_event_listener(listener_message); + }), + feature = "web_sys" => ({ + [ + EventListener::new(&ws, "open", listener_open), + EventListener::new(&ws, "close", listener_close), + EventListener::new(&ws, "error", listener_error), + EventListener::new(&ws, "message", listener_message), + ] + }), + }; + Ok(WebSocketTask { + ws, + notification, + #[cfg(feature = "web_sys")] + listeners, + }) + } } } @@ -158,10 +159,10 @@ impl WebSocketTask { IN: Into, { if let Ok(body) = data.into() { - #[cfg(feature = "std_web")] - let result = self.ws.send_text(&body); - #[cfg(feature = "web_sys")] - let result = self.ws.send_with_str(&body); + let result = cfg_match! { + feature = "std_web" => self.ws.send_text(&body), + feature = "web_sys" => self.ws.send_with_str(&body), + }; if result.is_err() { self.notification.emit(WebSocketStatus::Error); @@ -175,12 +176,12 @@ impl WebSocketTask { IN: Into, { if let Ok(body) = data.into() { - #[cfg(feature = "std_web")] - let result = self.ws.send_bytes(&body); - #[cfg(feature = "web_sys")] - let result = { - let mut body = body; - self.ws.send_with_u8_array(&mut body) + let result = cfg_match! { + feature = "std_web" => self.ws.send_bytes(&body), + feature = "web_sys" => ({ + let mut body = body; + self.ws.send_with_u8_array(&mut body) + }), }; if result.is_err() { @@ -192,20 +193,16 @@ impl WebSocketTask { impl Task for WebSocketTask { fn is_active(&self) -> bool { - #[cfg(feature = "std_web")] - { - self.ws.ready_state() == SocketReadyState::Open - } - #[cfg(feature = "web_sys")] - { - self.ws.ready_state() == WebSocket::OPEN + cfg_match! { + feature = "std_web" => self.ws.ready_state() == SocketReadyState::Open, + feature = "web_sys" => self.ws.ready_state() == WebSocket::OPEN, } } fn cancel(&mut self) { - #[cfg(feature = "std_web")] - self.ws.close(); - #[cfg(feature = "web_sys")] - self.ws.close().unwrap(); + cfg_match! { + feature = "std_web" => self.ws.close(), + feature = "web_sys" => self.ws.close().unwrap(), + }; } } From 74825a3e1d46ad8066c7734c5d5eb837421ba062 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Thu, 9 Jan 2020 17:00:32 +0100 Subject: [PATCH 08/11] Proper scoping. --- src/services/fetch.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/fetch.rs b/src/services/fetch.rs index 6c589addf7c..3a7e6ebb24b 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -621,7 +621,7 @@ where if let Some(abort_controller) = &abort_controller { init.signal(Some(&abort_controller.signal())); } - let promise = global!(global, global.fetch_with_request_and_init(&request, &init)) + let promise = crate::global!(global, global.fetch_with_request_and_init(&request, &init)) .then(&closure_then) .catch(&closure_catch); sender.send(closure_then).unwrap(); From 231a025a941aeaa59520b4f9635d500d439cf645 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sun, 12 Jan 2020 11:41:06 +0100 Subject: [PATCH 09/11] Some fixes. --- src/services/interval.rs | 1 + src/services/mod.rs | 12 +++++++++++- src/services/websocket.rs | 36 +++++++++++++++++++----------------- 3 files changed, 31 insertions(+), 18 deletions(-) diff --git a/src/services/interval.rs b/src/services/interval.rs index c29b0819800..95c60b25005 100644 --- a/src/services/interval.rs +++ b/src/services/interval.rs @@ -82,6 +82,7 @@ impl Task for IntervalTask { } } +#[cfg(feature = "std_web")] impl Drop for IntervalTask { fn drop(&mut self) { if self.is_active() { diff --git a/src/services/mod.rs b/src/services/mod.rs index 350349e2566..30851731cc9 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -28,15 +28,25 @@ pub use self::websocket::WebSocketService; use std::time::Duration; +#[doc(hidden)] +#[cfg(feature = "std_web")] +pub trait CfgDrop: Drop {} + +#[doc(hidden)] +#[cfg(feature = "web_sys")] +pub trait CfgDrop {} + /// An universal task of a service. /// It have to be canceled when dropped. -pub trait Task: Drop { +pub trait Task: CfgDrop { /// Returns `true` if task is active. fn is_active(&self) -> bool; /// Cancel current service's routine. fn cancel(&mut self); } +impl CfgDrop for T where T: Task {} + #[doc(hidden)] fn to_ms(duration: Duration) -> u32 { let ms = duration.subsec_millis(); diff --git a/src/services/websocket.rs b/src/services/websocket.rs index d40ff298164..e37b367bffa 100644 --- a/src/services/websocket.rs +++ b/src/services/websocket.rs @@ -37,8 +37,7 @@ pub struct WebSocketTask { ws: WebSocket, notification: Callback, #[cfg(feature = "web_sys")] - #[allow(dead_code)] - listeners: [EventListener; 4], + listeners: Option<[EventListener; 4]>, } impl fmt::Debug for WebSocketTask { @@ -101,24 +100,24 @@ impl WebSocketService { #[cfg(feature = "web_sys")] event: &Event| { #[cfg(feature = "web_sys")] let data = event.dyn_ref::().unwrap().data(); - #[cfg(feature = "std_web")] - let text = event.data().into_text(); - #[cfg(feature = "web_sys")] - let text = data.as_string(); - #[cfg(feature = "std_web")] - let bytes = event.data().into_array_buffer(); - #[cfg(feature = "web_sys")] - let bytes = Some(data); + let text = cfg_match! { + feature = "std_web" => event.data().into_text(), + feature = "web_sys" => data.as_string(), + }; + let bytes = cfg_match! { + feature = "std_web" => event.data().into_array_buffer(), + feature = "web_sys" => Some(data), + }; if let Some(text) = text { let data = Ok(text); let out = OUT::from(data); callback.emit(out); } else if let Some(bytes) = bytes { - #[cfg(feature = "std_web")] - let bytes: Vec = bytes.into(); - #[cfg(feature = "web_sys")] - let bytes = Uint8Array::new_with_byte_offset(&bytes, 0).to_vec(); + let bytes: Vec = cfg_match! { + feature = "std_web" => bytes.into(), + feature = "web_sys" => Uint8Array::new_with_byte_offset(&bytes, 0).to_vec(), + }; let data = Ok(bytes); let out = OUT::from(data); callback.emit(out); @@ -134,12 +133,12 @@ impl WebSocketService { ws.add_event_listener(listener_message); }), feature = "web_sys" => ({ - [ + Some([ EventListener::new(&ws, "open", listener_open), EventListener::new(&ws, "close", listener_close), EventListener::new(&ws, "error", listener_error), EventListener::new(&ws, "message", listener_message), - ] + ]) }), }; Ok(WebSocketTask { @@ -201,7 +200,10 @@ impl Task for WebSocketTask { fn cancel(&mut self) { cfg_match! { feature = "std_web" => self.ws.close(), - feature = "web_sys" => self.ws.close().unwrap(), + feature = "web_sys" => ({ + self.ws.close().unwrap(); + drop(self.listeners.take().expect("tried to cancel websocket twice")); + }), }; } } From f4c6e1d36f5463dd972f3b5aef35fd97fd6f36ed Mon Sep 17 00:00:00 2001 From: daxpedda Date: Sun, 12 Jan 2020 12:28:10 +0100 Subject: [PATCH 10/11] Move fetch and reader services to different PR. --- src/services/fetch.rs | 705 +---------------------------------------- src/services/reader.rs | 254 +-------------- 2 files changed, 10 insertions(+), 949 deletions(-) diff --git a/src/services/fetch.rs b/src/services/fetch.rs index 3a7e6ebb24b..b7acecae288 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -1,706 +1,11 @@ //! Service to send HTTP-request to a server. -use super::Task; -use crate::callback::Callback; -use crate::format::{Binary, Format, Text}; -use cfg_if::cfg_if; -use cfg_match::cfg_match; -use failure::Fail; -use std::fmt; -cfg_if! { +cfg_if::cfg_if! { if #[cfg(feature = "std_web")] { - use serde::Serialize; - use std::collections::HashMap; - use stdweb::serde::Serde; - use stdweb::unstable::{TryFrom, TryInto}; - use stdweb::web::ArrayBuffer; - use stdweb::{JsSerialize, Value}; - #[allow(unused_imports)] - use stdweb::{_js_impl, js}; + mod std_web; + pub use std_web::*; } else if #[cfg(feature = "web_sys")] { - use js_sys::{Array, Promise, Uint8Array}; - use std::rc::Rc; - use std::sync::atomic::{AtomicBool, Ordering}; - use std::sync::mpsc::{self, Receiver}; - use wasm_bindgen::{closure::Closure, JsValue}; - use web_sys::{ - AbortController, Headers, Request as WebRequest, RequestInit, Response as WebResponse, - }; - pub use web_sys::{ - RequestCache as Cache, RequestCredentials as Credentials, RequestMode as Mode, - RequestRedirect as Redirect, - }; - } -} - -pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri}; - -#[cfg(feature = "web_sys")] -struct ArrayBuffer(Uint8Array); - -#[cfg(feature = "web_sys")] -impl From for Vec { - fn from(from: ArrayBuffer) -> Self { - from.0.to_vec() - } -} - -#[cfg(feature = "web_sys")] -impl From for ArrayBuffer { - fn from(from: JsValue) -> Self { - ArrayBuffer(Uint8Array::new_with_byte_offset(&from, 0)) - } -} - -/// Type to set cache for fetch. -#[cfg(feature = "std_web")] -#[derive(Serialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum Cache { - /// `default` value of cache. - #[serde(rename = "default")] - DefaultCache, - /// `no-store` value of cache. - NoStore, - /// `reload` value of cache. - Reload, - /// `no-cache` value of cache. - NoCache, - /// `force-cache` value of cache - ForceCache, - /// `only-if-cached` value of cache - OnlyIfCached, -} - -/// Type to set credentials for fetch. -#[cfg(feature = "std_web")] -#[derive(Serialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum Credentials { - /// `omit` value of credentials. - Omit, - /// `include` value of credentials. - Include, - /// `same-origin` value of credentials. - SameOrigin, -} - -/// Type to set mode for fetch. -#[cfg(feature = "std_web")] -#[derive(Serialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum Mode { - /// `same-origin` value of mode. - SameOrigin, - /// `no-cors` value of mode. - NoCors, - /// `cors` value of mode. - Cors, -} - -/// Type to set redirect behaviour for fetch. -#[cfg(feature = "std_web")] -#[derive(Serialize, Debug)] -#[serde(rename_all = "kebab-case")] -pub enum Redirect { - /// `follow` value of redirect. - Follow, - /// `error` value of redirect. - Error, - /// `manual` value of redirect. - Manual, -} - -/// Init options for `fetch()` function call. -/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch -#[cfg_attr(feature = "std_web", derive(Serialize))] -#[derive(Default, Debug)] -pub struct FetchOptions { - /// Cache of a fetch request. - #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] - pub cache: Option, - /// Credentials of a fetch request. - #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] - pub credentials: Option, - /// Redirect behaviour of a fetch request. - #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] - pub redirect: Option, - /// Request mode of a fetch request. - #[cfg_attr(feature = "std_web", serde(skip_serializing_if = "Option::is_none"))] - pub mode: Option, -} - -#[cfg(feature = "web_sys")] -impl Into for FetchOptions { - fn into(self) -> RequestInit { - let mut init = RequestInit::new(); - - if let Some(cache) = self.cache { - init.cache(cache); - } - - if let Some(credentials) = self.credentials { - init.credentials(credentials); - } - - if let Some(redirect) = self.redirect { - init.redirect(redirect); - } - - if let Some(mode) = self.mode { - init.mode(mode); - } - - init - } -} - -/// Represents errors of a fetch service. -#[derive(Debug, Fail)] -enum FetchError { - #[fail(display = "failed response")] - FailedResponse, -} - -#[cfg(feature = "web_sys")] -#[derive(Debug)] -struct Handle { - active: Rc, - callbacks: Receiver>, - abort_controller: Option, - promise: Promise, -} - -/// A handle to control sent requests. Can be canceled with a `Task::cancel` call. -#[must_use] -pub struct FetchTask( - #[cfg(feature = "std_web")] Option, - #[cfg(feature = "web_sys")] Option, -); - -impl fmt::Debug for FetchTask { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("FetchTask") - } -} - -/// A service to fetch resources. -#[derive(Default, Debug)] -pub struct FetchService {} - -impl FetchService { - /// Creates a new service instance connected to `App` by provided `sender`. - pub fn new() -> Self { - Self {} - } - - /// Sends a request to a remote server given a Request object and a callback - /// fuction to convert a Response object into a loop's message. - /// - /// You may use a Request builder to build your request declaratively as on the - /// following examples: - /// - /// ``` - ///# use yew::format::{Nothing, Json}; - ///# use yew::services::fetch::Request; - ///# use serde_json::json; - /// let post_request = Request::post("https://my.api/v1/resource") - /// .header("Content-Type", "application/json") - /// .body(Json(&json!({"foo": "bar"}))) - /// .expect("Failed to build request."); - /// - /// let get_request = Request::get("https://my.api/v1/resource") - /// .body(Nothing) - /// .expect("Failed to build request."); - /// ``` - /// - /// The callback function can build a loop message by passing or analizing the - /// response body and metadata. - /// - /// ``` - ///# use yew::{Component, ComponentLink, Html, Renderable}; - ///# use yew::services::FetchService; - ///# use yew::services::fetch::{Response, Request}; - ///# struct Comp; - ///# impl Component for Comp { - ///# type Message = Msg;type Properties = (); - ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()} - ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()} - ///# fn view(&self) -> Html {unimplemented!()} - ///# } - ///# enum Msg { - ///# Noop, - ///# Error - ///# } - ///# fn dont_execute() { - ///# let link: ComponentLink = unimplemented!(); - ///# let mut fetch_service: FetchService = FetchService::new(); - ///# let post_request: Request> = unimplemented!(); - /// let task = fetch_service.fetch( - /// post_request, - /// link.callback(|response: Response>| { - /// if response.status().is_success() { - /// Msg::Noop - /// } else { - /// Msg::Error - /// } - /// }), - /// ); - ///# } - /// ``` - /// - /// For a full example, you can specify that the response must be in the JSON format, - /// and be a specific serialized data type. If the mesage isn't Json, or isn't the specified - /// data type, then you will get a message indicating failure. - /// - /// ``` - ///# use yew::format::{Json, Nothing, Format}; - ///# use yew::services::FetchService; - ///# use http::Request; - ///# use yew::services::fetch::Response; - ///# use yew::{Component, ComponentLink, Renderable, Html}; - ///# use serde_derive::Deserialize; - ///# struct Comp; - ///# impl Component for Comp { - ///# type Message = Msg;type Properties = (); - ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()} - ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()} - ///# fn view(&self) -> Html {unimplemented!()} - ///# } - ///# enum Msg { - ///# FetchResourceComplete(Data), - ///# FetchResourceFailed - ///# } - /// #[derive(Deserialize)] - /// struct Data { - /// value: String - /// } - /// - ///# fn dont_execute() { - ///# let link: ComponentLink = unimplemented!(); - /// let get_request = Request::get("/thing").body(Nothing).unwrap(); - /// let callback = link.callback(|response: Response>>| { - /// if let (meta, Json(Ok(body))) = response.into_parts() { - /// if meta.status.is_success() { - /// return Msg::FetchResourceComplete(body); - /// } - /// } - /// Msg::FetchResourceFailed - /// }); - /// - /// let task = FetchService::new().fetch(get_request, callback); - ///# } - /// ``` - /// - pub fn fetch( - &mut self, - request: Request, - callback: Callback>, - ) -> FetchTask - where - IN: Into, - OUT: From, - { - cfg_match! { - feature = "std_web" => fetch_impl::(false, request, None, callback), - feature = "web_sys" => ({ - fetch_impl::( - false, - request, - None, - callback, - Into::into, - |v| v.as_string().unwrap(), - ) - }), - } - } - - /// `fetch` with provided `FetchOptions` object. - /// Use it if you need to send cookies with a request: - /// ``` - ///# use yew::format::Nothing; - ///# use yew::services::fetch::{self, FetchOptions, Credentials}; - ///# use yew::{Renderable, Html, Component, ComponentLink}; - ///# use yew::services::FetchService; - ///# use http::Response; - ///# struct Comp; - ///# impl Component for Comp { - ///# type Message = Msg; - ///# type Properties = (); - ///# fn create(props: Self::Properties, link: ComponentLink) -> Self {unimplemented!()} - ///# fn update(&mut self, msg: Self::Message) -> bool {unimplemented!()} - ///# fn view(&self) -> Html {unimplemented!()} - ///# } - ///# pub enum Msg {} - ///# fn dont_execute() { - ///# let link: ComponentLink = unimplemented!(); - ///# let callback = link.callback(|response: Response>| unimplemented!()); - /// let request = fetch::Request::get("/path/") - /// .body(Nothing) - /// .unwrap(); - /// let options = FetchOptions { - /// credentials: Some(Credentials::SameOrigin), - /// ..FetchOptions::default() - /// }; - /// let task = FetchService::new().fetch_with_options(request, options, callback); - ///# } - /// ``` - pub fn fetch_with_options( - &mut self, - request: Request, - options: FetchOptions, - callback: Callback>, - ) -> FetchTask - where - IN: Into, - OUT: From, - { - cfg_match! { - feature = "std_web" => fetch_impl::(false, request, Some(options), callback), - feature = "web_sys" => ({ - fetch_impl::( - false, - request, - Some(options), - callback, - Into::into, - |v| v.as_string().unwrap(), - ) - }), - } - } - - /// Fetch the data in binary format. - pub fn fetch_binary( - &mut self, - request: Request, - callback: Callback>, - ) -> FetchTask - where - IN: Into, - OUT: From, - { - cfg_match! { - feature = "std_web" => fetch_impl::, ArrayBuffer>(true, request, None, callback), - feature = "web_sys" => ({ - fetch_impl::, ArrayBuffer, _, _>( - true, - request, - None, - callback, - |v| Uint8Array::from(v.as_slice()).into(), - From::from, - ) - }), - } - } - - /// Fetch the data in binary format. - pub fn fetch_binary_with_options( - &mut self, - request: Request, - options: FetchOptions, - callback: Callback>, - ) -> FetchTask - where - IN: Into, - OUT: From, - { - cfg_match! { - feature = "std_web" => fetch_impl::, ArrayBuffer>(true, request, Some(options), callback), - feature = "web_sys" => ({ - fetch_impl::, ArrayBuffer, _, _>( - true, - request, - Some(options), - callback, - |v| Uint8Array::from(v.as_slice()).into(), - From::from, - ) - }), - } - } -} - -fn fetch_impl< - IN, - OUT: 'static, - #[cfg(feature = "std_web")] T: JsSerialize, - #[cfg(feature = "web_sys")] T, - #[cfg(feature = "std_web")] X: TryFrom + Into, - #[cfg(feature = "web_sys")] X: Into, - #[cfg(feature = "web_sys")] IC: Fn(T) -> JsValue, - #[cfg(feature = "web_sys")] FC: 'static + Fn(JsValue) -> X, ->( - binary: bool, - request: Request, - options: Option, - callback: Callback>, - #[cfg(feature = "web_sys")] into_conversion: IC, - #[cfg(feature = "web_sys")] from_conversion: FC, -) -> FetchTask -where - IN: Into>, - OUT: From>, -{ - // Consume request as parts and body. - let (parts, body) = request.into_parts(); - - // Map headers into a Js serializable HashMap. - let header_map = parts.headers.iter().map(|(k, v)| { - ( - k.as_str(), - v.to_str() - .unwrap_or_else(|_| panic!("Unparsable request header {}: {:?}", k.as_str(), v)), - ) - }); - let header_map = cfg_match! { - feature = "std_web" => header_map.collect::>(), - feature = "web_sys" => ({ - let headers = Headers::new().unwrap(); - for (k, v) in header_map { - headers.append(k, v).unwrap(); - } - headers - }), - }; - // Formats URI. - let uri = format!("{}", parts.uri); - let method = parts.method.as_str(); - let body = body.into().ok(); - - // Prepare the response callback. - // Notice that the callback signature must match the call from the javascript - // side. There is no static check at this point. - let callback = move |#[cfg(feature = "std_web")] success: bool, - #[cfg(feature = "web_sys")] data: Option, - status: u16, - #[cfg(feature = "std_web")] headers: HashMap, - #[cfg(feature = "web_sys")] headers: Headers, - #[cfg(feature = "std_web")] data: X| { - let mut response_builder = Response::builder().status(status); - // convert `headers` to `Iterator` - let headers = cfg_match! { - feature = "std_web" => headers.into_iter(), - feature = "web_sys" => ({ - js_sys::try_iter(&headers) - .unwrap() - .unwrap() - .map(Result::unwrap) - .map(|entry| { - let entry = Array::from(&entry); - let key = entry.get(0); - let value = entry.get(1); - (key.as_string().unwrap(), value.as_string().unwrap()) - }) - }), - }; - for (key, value) in headers { - response_builder = response_builder.header(key.as_str(), value.as_str()); - } - - // Deserialize and wrap response data into a Text object. - #[cfg(feature = "std_web")] - let data = Some(data).filter(|_| success); - let data = if let Some(data) = data { - Ok(data.into()) - } else { - Err(FetchError::FailedResponse.into()) - }; - let out = OUT::from(data); - let response = response_builder.body(out).unwrap(); - callback.emit(response); - }; - - #[allow(clippy::too_many_arguments)] - let handle = cfg_match! { - feature = "std_web" => js! { - var body = @{body}; - if (@{binary} && body != null) { - body = Uint8Array.from(body); - } - var data = { - method: @{method}, - body: body, - headers: @{header_map}, - }; - var request = new Request(@{uri}, data); - var callback = @{callback}; - var abortController = AbortController ? new AbortController() : null; - var handle = { - active: true, - callback, - abortController, - }; - var init = @{Serde(options)} || {}; - if (abortController && !("signal" in init)) { - init.signal = abortController.signal; - } - fetch(request, init).then(function(response) { - var promise = (@{binary}) ? response.arrayBuffer() : response.text(); - var status = response.status; - var headers = {}; - response.headers.forEach(function(value, key) { - headers[key] = value; - }); - promise.then(function(data) { - if (handle.active == true) { - handle.active = false; - callback(true, status, headers, data); - callback.drop(); - } - }).catch(function(err) { - if (handle.active == true) { - handle.active = false; - callback(false, status, headers, data); - callback.drop(); - } - }); - }).catch(function(e) { - if (handle.active == true) { - var data = (@{binary}) ? new ArrayBuffer() : ""; - handle.active = false; - callback(false, 408, {}, data); - callback.drop(); - } - }); - return handle; - }, - feature = "web_sys" => ({ - let mut data = RequestInit::new(); - data.method(method); - data.body(body.map(into_conversion).as_ref()); - data.headers(&header_map); - let request = WebRequest::new_with_str_and_init(&uri, &data).unwrap(); - let active = Rc::new(AtomicBool::new(true)); - let (sender, receiver) = mpsc::channel(); - let active_outer_clone = Rc::clone(&active); - let callback_outer_clone = callback.clone(); - let sender_clone = sender.clone(); - let closure_then = move |response: JsValue| { - let response = WebResponse::from(response); - let promise = if binary { - response.array_buffer() - } else { - response.text() - } - .unwrap(); - let status = response.status(); - let headers = response.headers(); - let active_clone = Rc::clone(&active_outer_clone); - let callback_clone = callback_outer_clone.clone(); - let headers_clone = headers.clone(); - let closure_then = move |data: JsValue| { - let data = from_conversion(data); - if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { - callback_clone(Some(data), status, headers_clone); - } - }; - let closure_then = Closure::once(closure_then); - let closure_catch = move |_| { - if active_outer_clone.compare_and_swap(true, false, Ordering::SeqCst) { - callback_outer_clone(None, status, headers); - } - }; - let closure_catch = Closure::once(closure_catch); - promise.then(&closure_then).catch(&closure_catch); - sender_clone.send(closure_then).unwrap(); - sender_clone.send(closure_catch).unwrap(); - }; - let closure_then = Closure::once(closure_then); - let active_clone = Rc::clone(&active); - let closure_catch = move |_| { - if active_clone.compare_and_swap(true, false, Ordering::SeqCst) { - callback(None, 408, Headers::new().unwrap()); - } - }; - let closure_catch = Closure::wrap(Box::new(closure_catch) as Box); - let abort_controller = AbortController::new().ok(); - let mut init = options.map_or_else(RequestInit::new, Into::into); - if let Some(abort_controller) = &abort_controller { - init.signal(Some(&abort_controller.signal())); - } - let promise = crate::global!(global, global.fetch_with_request_and_init(&request, &init)) - .then(&closure_then) - .catch(&closure_catch); - sender.send(closure_then).unwrap(); - sender.send(closure_catch).unwrap(); - Handle { - active, - callbacks: receiver, - abort_controller, - promise, - } - }), - }; - FetchTask(Some(handle)) -} - -impl Task for FetchTask { - fn is_active(&self) -> bool { - if let Some(ref task) = self.0 { - cfg_match! { - feature = "std_web" => ({ - let result = js! { - var the_task = @{task}; - return the_task.active && - (!the_task.abortController || !the_task.abortController.signal.aborted); - }; - result.try_into().unwrap_or(false) - }), - feature = "web_sys" => ({ - task.active.load(Ordering::SeqCst) - && task - .abort_controller - .as_ref() - .map(|abort_controller| abort_controller.signal().aborted()) - .filter(|value| *value) - .is_none() - }), - } - } else { - false - } - } - fn cancel(&mut self) { - // Fetch API doesn't support request cancelling in all browsers - // and we should use this workaround with a flag. - // In that case, request not canceled, but callback won't be called. - let handle = self - .0 - .take() - .expect("tried to cancel request fetching twice"); - cfg_match! { - feature = "std_web" => ({ - js! { @(no_return) - var handle = @{handle}; - handle.active = false; - handle.callback.drop(); - if (handle.abortController) { - handle.abortController.abort(); - } - }; - }), - feature = "web_sys" => ({ - thread_local! { - static CATCH: Closure = Closure::wrap(Box::new(|_| ()) as Box); - } - handle.active.store(false, Ordering::SeqCst); - CATCH.with(|c| handle.promise.catch(&c)); - if let Some(abort_controller) = handle.abort_controller { - abort_controller.abort(); - } - handle.callbacks.try_iter().for_each(drop); - }), - }; - } -} - -impl Drop for FetchTask { - fn drop(&mut self) { - if self.is_active() { - self.cancel(); - } + mod web_sys; + pub use self::web_sys::*; } } diff --git a/src/services/reader.rs b/src/services/reader.rs index 20fb55b9752..3fc05afffda 100644 --- a/src/services/reader.rs +++ b/src/services/reader.rs @@ -1,255 +1,11 @@ //! Service to load files using `FileReader`. -use super::Task; -use crate::callback::Callback; -use cfg_if::cfg_if; -use cfg_match::cfg_match; -use std::cmp; -use std::fmt; -cfg_if! { +cfg_if::cfg_if! { if #[cfg(feature = "std_web")] { - pub use stdweb::web::{Blob, File, IBlob}; - #[allow(unused_imports)] - use stdweb::{_js_impl, js}; - use stdweb::unstable::TryInto; - use stdweb::web::event::LoadEndEvent; - use stdweb::web::{FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, TypedArray}; + mod std_web; + pub use std_web::*; } else if #[cfg(feature = "web_sys")] { - pub use web_sys::{Blob, File}; - use gloo::events::EventListener; - use js_sys::Uint8Array; - use web_sys::{Event, FileReader}; - } -} - -/// Struct that represents data of a file. -#[derive(Clone, Debug)] -pub struct FileData { - /// Name of loaded file. - pub name: String, - /// Content of loaded file. - pub content: Vec, -} - -/// Struct that represents a chunk of a file. -#[derive(Clone, Debug)] -pub enum FileChunk { - /// Reading of chunks started. Equals **0%** progress. - Started { - /// Name of loaded file. - name: String, - }, - /// The next data chunk that read. Also provides a progress value. - DataChunk { - /// The chunk of binary data. - data: Vec, - /// The progress value in interval: `0 < progress <= 1`. - progress: f32, - }, - /// Reading of chunks finished. Equals **100%** progress. - Finished, -} - -/// A reader service attached to a user context. -#[derive(Default, Debug)] -pub struct ReaderService {} - -impl ReaderService { - /// Creates a new service instance connected to `App` by provided `sender`. - pub fn new() -> Self { - Self {} - } - - /// Reads all bytes from a file and returns them with a callback. - pub fn read_file(&mut self, file: File, callback: Callback) -> ReaderTask { - let file_reader = FileReader::new(); - #[cfg(feature = "web_sys")] - let file_reader = file_reader.unwrap(); - let reader = file_reader.clone(); - let name = file.name(); - let callback = move |#[cfg(feature = "std_web")] _event: LoadEndEvent, - #[cfg(feature = "web_sys")] _event: &Event| { - cfg_match! { - feature = "std_web" => ({ - match reader.result() { - Some(FileReaderResult::String(_)) => { - unreachable!(); - } - Some(FileReaderResult::ArrayBuffer(buffer)) => { - let array: TypedArray = buffer.into(); - let data = FileData { - name: name.clone(), - content: array.to_vec(), - }; - callback.emit(data); - } - None => {} - } - }), - feature = "web_sys" => ({ - let array = Uint8Array::new_with_byte_offset( - &reader - .result() - .expect("`FileReader` hasn't finished loading"), - 0, - ); - let data = FileData { - name: name.clone(), - content: array.to_vec(), - }; - callback.emit(data); - }), - }; - }; - #[cfg_attr(feature = "std_web", allow(unused_variables))] - let listener = cfg_match! { - feature = "std_web" => file_reader.add_event_listener(callback), - feature = "web_sys" => EventListener::new(&file_reader, "loadend", callback), - }; - file_reader.read_as_array_buffer(&file).unwrap(); - ReaderTask { - file_reader, - #[cfg(feature = "web_sys")] - listener, - } - } - - /// Reads data chunks from a file and returns them with a callback. - pub fn read_file_by_chunks( - &mut self, - file: File, - callback: Callback, - chunk_size: usize, - ) -> ReaderTask { - let file_reader = FileReader::new(); - #[cfg(feature = "web_sys")] - let file_reader = file_reader.unwrap(); - let name = file.name(); - let mut position = 0; - let total_size = cfg_match! { - feature = "std_web" => file.len(), - feature = "web_sys" => file.size(), - } as usize; - let reader = file_reader.clone(); - let callback = move |#[cfg(feature = "std_web")] _event: LoadEndEvent, - #[cfg(feature = "web_sys")] _event: &Event| { - cfg_match! { - feature = "std_web" => ({ - match reader.result() { - // This branch is used to start reading - Some(FileReaderResult::String(_)) => { - let started = FileChunk::Started { name: name.clone() }; - callback.emit(started); - } - // This branch is used to send a chunk value - Some(FileReaderResult::ArrayBuffer(buffer)) => { - let array: TypedArray = buffer.into(); - let chunk = FileChunk::DataChunk { - data: array.to_vec(), - progress: position as f32 / total_size as f32, - }; - callback.emit(chunk); - } - None => {} - } - }), - feature = "web_sys" => ({ - let result = reader - .result() - .expect("`FileReader` hasn't finished loading"); - - if result.is_string() { - let started = FileChunk::Started { name: name.clone() }; - callback.emit(started); - } else { - let array = Uint8Array::new_with_byte_offset(&result, 0); - let chunk = FileChunk::DataChunk { - data: array.to_vec(), - progress: position as f32 / total_size as f32, - }; - callback.emit(chunk); - } - }), - }; - // Read the next chunk - if position < total_size { - let from = position; - let to = cmp::min(position + chunk_size, total_size); - position = to; - let blob: Blob = cfg_match! { - feature = "std_web" => ({ - // TODO Implement `slice` method in `stdweb` - let file = &file; - (js! { - return @{file}.slice(@{from as u32}, @{to as u32}); - }) - .try_into() - .unwrap() - }), - feature = "web_sys" => file.slice_with_i32_and_i32(from as _, to as _).unwrap(), - }; - reader.read_as_array_buffer(&blob).unwrap(); - } else { - let finished = FileChunk::Finished; - callback.emit(finished); - } - }; - #[cfg_attr(feature = "std_web", allow(unused_variables))] - let listener = cfg_match! { - feature = "std_web" => file_reader.add_event_listener(callback), - feature = "web_sys" => EventListener::new(&file_reader, "loadend", callback), - }; - let blob: Blob = cfg_match! { - feature = "std_web" => ({ - (js! { - return (new Blob()); - }) - .try_into() - .unwrap() - }), - feature = "web_sys" => Blob::new().unwrap(), - }; - file_reader.read_as_text(&blob).unwrap(); - ReaderTask { - file_reader, - #[cfg(feature = "web_sys")] - listener, - } - } -} - -/// A handle to control reading. -#[must_use] -pub struct ReaderTask { - file_reader: FileReader, - #[cfg(feature = "web_sys")] - #[allow(dead_code)] - listener: EventListener, -} - -impl fmt::Debug for ReaderTask { - fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { - f.write_str("ReaderTask") - } -} - -impl Task for ReaderTask { - fn is_active(&self) -> bool { - cfg_match! { - feature = "std_web" => self.file_reader.ready_state() == FileReaderReadyState::Loading, - feature = "web_sys" => self.file_reader.ready_state() == FileReader::LOADING, - } - } - - fn cancel(&mut self) { - self.file_reader.abort(); - } -} - -impl Drop for ReaderTask { - fn drop(&mut self) { - if self.is_active() { - self.cancel(); - } + mod web_sys; + pub use self::web_sys::*; } } From 752492921b1c84072b5909b05bc2acdad59727c1 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Wed, 15 Jan 2020 15:01:51 +0100 Subject: [PATCH 11/11] Revert split. --- src/services/fetch.rs | 451 ++++++++++++++++++++++++++++++++++++++- src/services/interval.rs | 8 +- src/services/mod.rs | 16 +- src/services/reader.rs | 164 +++++++++++++- 4 files changed, 611 insertions(+), 28 deletions(-) diff --git a/src/services/fetch.rs b/src/services/fetch.rs index b7acecae288..00f6d90070b 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -1,11 +1,448 @@ //! Service to send HTTP-request to a server. -cfg_if::cfg_if! { - if #[cfg(feature = "std_web")] { - mod std_web; - pub use std_web::*; - } else if #[cfg(feature = "web_sys")] { - mod web_sys; - pub use self::web_sys::*; +use super::Task; +use crate::callback::Callback; +use crate::format::{Binary, Format, Text}; +use serde::Serialize; +use std::collections::HashMap; +use std::fmt; +use stdweb::serde::Serde; +use stdweb::unstable::{TryFrom, TryInto}; +use stdweb::web::ArrayBuffer; +use stdweb::{JsSerialize, Value}; +#[allow(unused_imports)] +use stdweb::{_js_impl, js}; +use thiserror::Error; + +pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri}; + +/// Type to set cache for fetch. +#[derive(Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum Cache { + /// `default` value of cache. + #[serde(rename = "default")] + DefaultCache, + /// `no-store` value of cache. + NoStore, + /// `reload` value of cache. + Reload, + /// `no-cache` value of cache. + NoCache, + /// `force-cache` value of cache + ForceCache, + /// `only-if-cached` value of cache + OnlyIfCached, +} + +/// Type to set credentials for fetch. +#[derive(Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum Credentials { + /// `omit` value of credentials. + Omit, + /// `include` value of credentials. + Include, + /// `same-origin` value of credentials. + SameOrigin, +} + +/// Type to set mode for fetch. +#[derive(Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum Mode { + /// `same-origin` value of mode. + SameOrigin, + /// `no-cors` value of mode. + NoCors, + /// `cors` value of mode. + Cors, +} + +/// Type to set redirect behaviour for fetch. +#[derive(Serialize, Debug)] +#[serde(rename_all = "kebab-case")] +pub enum Redirect { + /// `follow` value of redirect. + Follow, + /// `error` value of redirect. + Error, + /// `manual` value of redirect. + Manual, +} + +/// Init options for `fetch()` function call. +/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch +#[derive(Serialize, Default, Debug)] +pub struct FetchOptions { + /// Cache of a fetch request. + #[serde(skip_serializing_if = "Option::is_none")] + pub cache: Option, + /// Credentials of a fetch request. + #[serde(skip_serializing_if = "Option::is_none")] + pub credentials: Option, + /// Redirect behaviour of a fetch request. + #[serde(skip_serializing_if = "Option::is_none")] + pub redirect: Option, + /// Request mode of a fetch request. + #[serde(skip_serializing_if = "Option::is_none")] + pub mode: Option, +} + +/// Represents errors of a fetch service. +#[derive(Debug, Error)] +enum FetchError { + #[error("failed response")] + FailedResponse, +} + +/// A handle to control sent requests. Can be canceled with a `Task::cancel` call. +#[must_use] +pub struct FetchTask(Option); + +impl fmt::Debug for FetchTask { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("FetchTask") + } +} + +/// A service to fetch resources. +#[derive(Default, Debug)] +pub struct FetchService {} + +impl FetchService { + /// Creates a new service instance connected to `App` by provided `sender`. + pub fn new() -> Self { + Self {} + } + + /// Sends a request to a remote server given a Request object and a callback + /// fuction to convert a Response object into a loop's message. + /// + /// You may use a Request builder to build your request declaratively as on the + /// following examples: + /// + /// ``` + ///# use yew::format::{Nothing, Json}; + ///# use yew::services::fetch::Request; + ///# use serde_json::json; + /// let post_request = Request::post("https://my.api/v1/resource") + /// .header("Content-Type", "application/json") + /// .body(Json(&json!({"foo": "bar"}))) + /// .expect("Failed to build request."); + /// + /// let get_request = Request::get("https://my.api/v1/resource") + /// .body(Nothing) + /// .expect("Failed to build request."); + /// ``` + /// + /// The callback function can build a loop message by passing or analizing the + /// response body and metadata. + /// + /// ``` + ///# use yew::{Component, ComponentLink, Html, Renderable}; + ///# use yew::services::FetchService; + ///# use yew::services::fetch::{Response, Request}; + ///# struct Comp; + ///# impl Component for Comp { + ///# type Message = Msg;type Properties = (); + ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()} + ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()} + ///# fn view(&self) -> Html {unimplemented!()} + ///# } + ///# enum Msg { + ///# Noop, + ///# Error + ///# } + ///# fn dont_execute() { + ///# let link: ComponentLink = unimplemented!(); + ///# let mut fetch_service: FetchService = FetchService::new(); + ///# let post_request: Request> = unimplemented!(); + /// let task = fetch_service.fetch( + /// post_request, + /// link.callback(|response: Response>| { + /// if response.status().is_success() { + /// Msg::Noop + /// } else { + /// Msg::Error + /// } + /// }), + /// ); + ///# } + /// ``` + /// + /// For a full example, you can specify that the response must be in the JSON format, + /// and be a specific serialized data type. If the mesage isn't Json, or isn't the specified + /// data type, then you will get a message indicating failure. + /// + /// ``` + ///# use yew::format::{Json, Nothing, Format}; + ///# use yew::services::FetchService; + ///# use http::Request; + ///# use yew::services::fetch::Response; + ///# use yew::{Component, ComponentLink, Renderable, Html}; + ///# use serde_derive::Deserialize; + ///# struct Comp; + ///# impl Component for Comp { + ///# type Message = Msg;type Properties = (); + ///# fn create(props: Self::Properties,link: ComponentLink) -> Self {unimplemented!()} + ///# fn update(&mut self,msg: Self::Message) -> bool {unimplemented!()} + ///# fn view(&self) -> Html {unimplemented!()} + ///# } + ///# enum Msg { + ///# FetchResourceComplete(Data), + ///# FetchResourceFailed + ///# } + /// #[derive(Deserialize)] + /// struct Data { + /// value: String + /// } + /// + ///# fn dont_execute() { + ///# let link: ComponentLink = unimplemented!(); + /// let get_request = Request::get("/thing").body(Nothing).unwrap(); + /// let callback = link.callback(|response: Response>>| { + /// if let (meta, Json(Ok(body))) = response.into_parts() { + /// if meta.status.is_success() { + /// return Msg::FetchResourceComplete(body); + /// } + /// } + /// Msg::FetchResourceFailed + /// }); + /// + /// let task = FetchService::new().fetch(get_request, callback); + ///# } + /// ``` + /// + pub fn fetch( + &mut self, + request: Request, + callback: Callback>, + ) -> FetchTask + where + IN: Into, + OUT: From, + { + fetch_impl::(false, request, None, callback) + } + + /// `fetch` with provided `FetchOptions` object. + /// Use it if you need to send cookies with a request: + /// ``` + ///# use yew::format::Nothing; + ///# use yew::services::fetch::{self, FetchOptions, Credentials}; + ///# use yew::{Renderable, Html, Component, ComponentLink}; + ///# use yew::services::FetchService; + ///# use http::Response; + ///# struct Comp; + ///# impl Component for Comp { + ///# type Message = Msg; + ///# type Properties = (); + ///# fn create(props: Self::Properties, link: ComponentLink) -> Self {unimplemented!()} + ///# fn update(&mut self, msg: Self::Message) -> bool {unimplemented!()} + ///# fn view(&self) -> Html {unimplemented!()} + ///# } + ///# pub enum Msg {} + ///# fn dont_execute() { + ///# let link: ComponentLink = unimplemented!(); + ///# let callback = link.callback(|response: Response>| unimplemented!()); + /// let request = fetch::Request::get("/path/") + /// .body(Nothing) + /// .unwrap(); + /// let options = FetchOptions { + /// credentials: Some(Credentials::SameOrigin), + /// ..FetchOptions::default() + /// }; + /// let task = FetchService::new().fetch_with_options(request, options, callback); + ///# } + /// ``` + pub fn fetch_with_options( + &mut self, + request: Request, + options: FetchOptions, + callback: Callback>, + ) -> FetchTask + where + IN: Into, + OUT: From, + { + fetch_impl::(false, request, Some(options), callback) + } + + /// Fetch the data in binary format. + pub fn fetch_binary( + &mut self, + request: Request, + callback: Callback>, + ) -> FetchTask + where + IN: Into, + OUT: From, + { + fetch_impl::, ArrayBuffer>(true, request, None, callback) + } + + /// Fetch the data in binary format. + pub fn fetch_binary_with_options( + &mut self, + request: Request, + options: FetchOptions, + callback: Callback>, + ) -> FetchTask + where + IN: Into, + OUT: From, + { + fetch_impl::, ArrayBuffer>(true, request, Some(options), callback) + } +} + +fn fetch_impl( + binary: bool, + request: Request, + options: Option, + callback: Callback>, +) -> FetchTask +where + IN: Into>, + OUT: From>, + T: JsSerialize, + X: TryFrom + Into, +{ + // Consume request as parts and body. + let (parts, body) = request.into_parts(); + + // Map headers into a Js serializable HashMap. + let header_map: HashMap<&str, &str> = parts + .headers + .iter() + .map(|(k, v)| { + ( + k.as_str(), + v.to_str().unwrap_or_else(|_| { + panic!("Unparsable request header {}: {:?}", k.as_str(), v) + }), + ) + }) + .collect(); + + // Formats URI. + let uri = format!("{}", parts.uri); + let method = parts.method.as_str(); + let body = body.into().ok(); + + // Prepare the response callback. + // Notice that the callback signature must match the call from the javascript + // side. There is no static check at this point. + let callback = move |success: bool, status: u16, headers: HashMap, data: X| { + let mut response_builder = Response::builder().status(status); + for (key, values) in headers { + response_builder = response_builder.header(key.as_str(), values.as_str()); + } + + // Deserialize and wrap response data into a Text object. + let data = if success { + Ok(data.into()) + } else { + Err(FetchError::FailedResponse.into()) + }; + let out = OUT::from(data); + let response = response_builder.body(out).unwrap(); + callback.emit(response); + }; + + #[allow(clippy::too_many_arguments)] + let handle = js! { + var body = @{body}; + if (@{binary} && body != null) { + body = Uint8Array.from(body); + } + var data = { + method: @{method}, + body: body, + headers: @{header_map}, + }; + var request = new Request(@{uri}, data); + var callback = @{callback}; + var abortController = AbortController ? new AbortController() : null; + var handle = { + active: true, + callback, + abortController, + }; + var init = @{Serde(options)} || {}; + if (abortController && !("signal" in init)) { + init.signal = abortController.signal; + } + fetch(request, init).then(function(response) { + var promise = (@{binary}) ? response.arrayBuffer() : response.text(); + var status = response.status; + var headers = {}; + response.headers.forEach(function(value, key) { + headers[key] = value; + }); + promise.then(function(data) { + if (handle.active == true) { + handle.active = false; + callback(true, status, headers, data); + callback.drop(); + } + }).catch(function(err) { + if (handle.active == true) { + handle.active = false; + callback(false, status, headers, data); + callback.drop(); + } + }); + }).catch(function(e) { + if (handle.active == true) { + var data = (@{binary}) ? new ArrayBuffer() : ""; + handle.active = false; + callback(false, 408, {}, data); + callback.drop(); + } + }); + return handle; + }; + FetchTask(Some(handle)) +} + +impl Task for FetchTask { + fn is_active(&self) -> bool { + if let Some(ref task) = self.0 { + let result = js! { + var the_task = @{task}; + return the_task.active && + (!the_task.abortController || !the_task.abortController.signal.aborted); + }; + result.try_into().unwrap_or(false) + } else { + false + } + } + fn cancel(&mut self) { + // Fetch API doesn't support request cancelling in all browsers + // and we should use this workaround with a flag. + // In that case, request not canceled, but callback won't be called. + let handle = self + .0 + .take() + .expect("tried to cancel request fetching twice"); + js! { @(no_return) + var handle = @{handle}; + handle.active = false; + handle.callback.drop(); + if (handle.abortController) { + handle.abortController.abort(); + } + } + } +} + +impl Drop for FetchTask { + fn drop(&mut self) { + if self.is_active() { + self.cancel(); + } } } diff --git a/src/services/interval.rs b/src/services/interval.rs index 95c60b25005..97d7e5eddab 100644 --- a/src/services/interval.rs +++ b/src/services/interval.rs @@ -82,11 +82,13 @@ impl Task for IntervalTask { } } -#[cfg(feature = "std_web")] impl Drop for IntervalTask { fn drop(&mut self) { - if self.is_active() { - self.cancel(); + #[cfg(feature = "std_web")] + { + if self.is_active() { + self.cancel(); + } } } } diff --git a/src/services/mod.rs b/src/services/mod.rs index 30851731cc9..a3cdbfffca8 100644 --- a/src/services/mod.rs +++ b/src/services/mod.rs @@ -5,9 +5,11 @@ pub mod console; pub mod dialog; +#[cfg(feature = "std_web")] pub mod fetch; pub mod interval; pub mod keyboard; +#[cfg(feature = "std_web")] pub mod reader; pub mod render; pub mod resize; @@ -17,8 +19,10 @@ pub mod websocket; pub use self::console::ConsoleService; pub use self::dialog::DialogService; +#[cfg(feature = "std_web")] pub use self::fetch::FetchService; pub use self::interval::IntervalService; +#[cfg(feature = "std_web")] pub use self::reader::ReaderService; pub use self::render::RenderService; pub use self::resize::ResizeService; @@ -28,25 +32,15 @@ pub use self::websocket::WebSocketService; use std::time::Duration; -#[doc(hidden)] -#[cfg(feature = "std_web")] -pub trait CfgDrop: Drop {} - -#[doc(hidden)] -#[cfg(feature = "web_sys")] -pub trait CfgDrop {} - /// An universal task of a service. /// It have to be canceled when dropped. -pub trait Task: CfgDrop { +pub trait Task: Drop { /// Returns `true` if task is active. fn is_active(&self) -> bool; /// Cancel current service's routine. fn cancel(&mut self); } -impl CfgDrop for T where T: Task {} - #[doc(hidden)] fn to_ms(duration: Duration) -> u32 { let ms = duration.subsec_millis(); diff --git a/src/services/reader.rs b/src/services/reader.rs index 3fc05afffda..c1fd144b098 100644 --- a/src/services/reader.rs +++ b/src/services/reader.rs @@ -1,11 +1,161 @@ //! Service to load files using `FileReader`. -cfg_if::cfg_if! { - if #[cfg(feature = "std_web")] { - mod std_web; - pub use std_web::*; - } else if #[cfg(feature = "web_sys")] { - mod web_sys; - pub use self::web_sys::*; +use super::Task; +use crate::callback::Callback; +use std::cmp; +use std::fmt; +use stdweb::unstable::TryInto; +use stdweb::web::event::LoadEndEvent; +pub use stdweb::web::{Blob, File, IBlob}; +use stdweb::web::{FileReader, FileReaderReadyState, FileReaderResult, IEventTarget, TypedArray}; +#[allow(unused_imports)] +use stdweb::{_js_impl, js}; + +/// Struct that represents data of a file. +#[derive(Clone, Debug)] +pub struct FileData { + /// Name of loaded file. + pub name: String, + /// Content of loaded file. + pub content: Vec, +} + +/// Struct that represents a chunk of a file. +#[derive(Clone, Debug)] +pub enum FileChunk { + /// Reading of chunks started. Equals **0%** progress. + Started { + /// Name of loaded file. + name: String, + }, + /// The next data chunk that read. Also provides a progress value. + DataChunk { + /// The chunk of binary data. + data: Vec, + /// The progress value in interval: `0 < progress <= 1`. + progress: f32, + }, + /// Reading of chunks finished. Equals **100%** progress. + Finished, +} + +/// A reader service attached to a user context. +#[derive(Default, Debug)] +pub struct ReaderService {} + +impl ReaderService { + /// Creates a new service instance connected to `App` by provided `sender`. + pub fn new() -> Self { + Self {} + } + + /// Reads all bytes from a file and returns them with a callback. + pub fn read_file(&mut self, file: File, callback: Callback) -> ReaderTask { + let file_reader = FileReader::new(); + let reader = file_reader.clone(); + let name = file.name(); + file_reader.add_event_listener(move |_event: LoadEndEvent| match reader.result() { + Some(FileReaderResult::String(_)) => { + unreachable!(); + } + Some(FileReaderResult::ArrayBuffer(buffer)) => { + let array: TypedArray = buffer.into(); + let data = FileData { + name: name.clone(), + content: array.to_vec(), + }; + callback.emit(data); + } + None => {} + }); + file_reader.read_as_array_buffer(&file).unwrap(); + ReaderTask { file_reader } + } + + /// Reads data chunks from a file and returns them with a callback. + pub fn read_file_by_chunks( + &mut self, + file: File, + callback: Callback, + chunk_size: usize, + ) -> ReaderTask { + let file_reader = FileReader::new(); + let name = file.name(); + let mut position = 0; + let total_size = file.len() as usize; + let reader = file_reader.clone(); + file_reader.add_event_listener(move |_event: LoadEndEvent| { + match reader.result() { + // This branch is used to start reading + Some(FileReaderResult::String(_)) => { + let started = FileChunk::Started { name: name.clone() }; + callback.emit(started); + } + // This branch is used to send a chunk value + Some(FileReaderResult::ArrayBuffer(buffer)) => { + let array: TypedArray = buffer.into(); + let chunk = FileChunk::DataChunk { + data: array.to_vec(), + progress: position as f32 / total_size as f32, + }; + callback.emit(chunk); + } + None => {} + } + // Read the next chunk + if position < total_size { + let file = &file; + let from = position; + let to = cmp::min(position + chunk_size, total_size); + position = to; + // TODO Implement `slice` method in `stdweb` + let blob: Blob = (js! { + return @{file}.slice(@{from as u32}, @{to as u32}); + }) + .try_into() + .unwrap(); + reader.read_as_array_buffer(&blob).unwrap(); + } else { + let finished = FileChunk::Finished; + callback.emit(finished); + } + }); + let blob: Blob = (js! { + return (new Blob()); + }) + .try_into() + .unwrap(); + file_reader.read_as_text(&blob).unwrap(); + ReaderTask { file_reader } + } +} + +/// A handle to control reading. +#[must_use] +pub struct ReaderTask { + file_reader: FileReader, +} + +impl fmt::Debug for ReaderTask { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str("ReaderTask") + } +} + +impl Task for ReaderTask { + fn is_active(&self) -> bool { + self.file_reader.ready_state() == FileReaderReadyState::Loading + } + + fn cancel(&mut self) { + self.file_reader.abort(); + } +} + +impl Drop for ReaderTask { + fn drop(&mut self) { + if self.is_active() { + self.cancel(); + } } }