From 6e3f101dbedde2142f041467a8ae40ef5e3920c5 Mon Sep 17 00:00:00 2001 From: daxpedda Date: Wed, 15 Jan 2020 15:18:06 +0100 Subject: [PATCH] Revert split. --- src/services/fetch.rs | 466 ++++++++++++++++++++++++------- src/services/fetch/std_web.rs | 448 ------------------------------ src/services/fetch/web_sys.rs | 509 ---------------------------------- 3 files changed, 368 insertions(+), 1055 deletions(-) delete mode 100644 src/services/fetch/std_web.rs delete mode 100644 src/services/fetch/web_sys.rs diff --git a/src/services/fetch.rs b/src/services/fetch.rs index 00f6d90070b..fdfcfa02a5d 100644 --- a/src/services/fetch.rs +++ b/src/services/fetch.rs @@ -3,20 +3,57 @@ use super::Task; use crate::callback::Callback; use crate::format::{Binary, Format, Text}; -use serde::Serialize; -use std::collections::HashMap; +use cfg_if::cfg_if; +use cfg_match::cfg_match; +use failure::Fail; 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; +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, Reflect, 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, Window, WorkerGlobalScope, + }; + } +} 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 { @@ -36,6 +73,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 +86,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 +99,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 +113,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, Error)] enum FetchError { @@ -96,9 +162,21 @@ enum FetchError { 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(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 +301,19 @@ impl FetchService { IN: Into, OUT: From, { - 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(), + ) + }), + } } /// `fetch` with provided `FetchOptions` object. @@ -266,7 +356,19 @@ impl FetchService { IN: Into, OUT: From, { - 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(), + ) + }), + } } /// Fetch the data in binary format. @@ -279,7 +381,19 @@ impl FetchService { IN: Into, OUT: From, { - 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, + ) + }), + } } /// Fetch the data in binary format. @@ -293,39 +407,64 @@ impl FetchService { IN: Into, OUT: From, { - fetch_impl::, ArrayBuffer>(true, request, Some(options), callback) + 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( +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)), + ) + }); + 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(); @@ -334,14 +473,37 @@ 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()); + // 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. - 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()) @@ -352,57 +514,134 @@ where }; #[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; + }, + 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); + #[allow(unused_must_use)] + { + 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 global: JsValue = js_sys::global().into(); + let promise = if Reflect::has(&global, &String::from("Window").into()).unwrap() { + Window::from(global).fetch_with_request_and_init(&request, &init) + } else if Reflect::has(&global, &String::from("WorkerGlobalScope").into()).unwrap() { + WorkerGlobalScope::from(global).fetch_with_request_and_init(&request, &init) + } else { + panic!("failed to get global context") + }; + let promise = promise.then(&closure_then).catch(&closure_catch); + sender.send(closure_then).unwrap(); + sender.send(closure_catch).unwrap(); + Handle { + active, + callbacks: receiver, + abort_controller, + promise, } - }); - return handle; + }), }; FetchTask(Some(handle)) } @@ -410,12 +649,25 @@ where 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_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 } @@ -428,14 +680,32 @@ impl Task for FetchTask { .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(); - } - } + 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); + #[allow(unused_must_use)] + { + 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/fetch/std_web.rs b/src/services/fetch/std_web.rs deleted file mode 100644 index 62711b2a2ef..00000000000 --- a/src/services/fetch/std_web.rs +++ /dev/null @@ -1,448 +0,0 @@ -//! `stdweb` implementation for the fetch service. - -use crate::callback::Callback; -use crate::format::{Binary, Format, Text}; -use crate::services::Task; -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}; -#[allow(unused_imports)] -use stdweb::{_js_impl, js}; - -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, Fail)] -enum FetchError { - #[fail(display = "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/fetch/web_sys.rs b/src/services/fetch/web_sys.rs deleted file mode 100644 index e486828d801..00000000000 --- a/src/services/fetch/web_sys.rs +++ /dev/null @@ -1,509 +0,0 @@ -//! `web-sys` implementation for the fetch service. - -use crate::callback::Callback; -use crate::format::{Binary, Format, Text}; -use crate::services::Task; -use failure::Fail; -use js_sys::Reflect; -use js_sys::{Array, Promise, Uint8Array}; -use std::fmt; -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, Window, WorkerGlobalScope, -}; - -pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri}; - -struct ArrayBuffer(Uint8Array); - -impl From for Vec { - fn from(from: ArrayBuffer) -> Self { - from.0.to_vec() - } -} - -impl From for ArrayBuffer { - fn from(from: JsValue) -> Self { - ArrayBuffer(Uint8Array::new_with_byte_offset(&from, 0)) - } -} - -/// Init options for `fetch()` function call. -/// https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/fetch -#[derive(Default, Debug)] -pub struct FetchOptions { - /// Cache of a fetch request. - pub cache: Option, - /// Credentials of a fetch request. - pub credentials: Option, - /// Redirect behaviour of a fetch request. - pub redirect: Option, - /// Request mode of a fetch request. - pub mode: Option, -} - -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, -} - -#[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(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, - 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, - { - 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, - { - 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, - { - fetch_impl::, ArrayBuffer, _, _>( - true, - request, - Some(options), - callback, - |v| Uint8Array::from(v.as_slice()).into(), - From::from, - ) - } -} - -fn fetch_impl< - IN, - OUT: 'static, - T, - X: Into, - IC: Fn(T) -> JsValue, - FC: 'static + Fn(JsValue) -> X, ->( - binary: bool, - request: Request, - options: Option, - callback: Callback>, - into_conversion: IC, - 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 = Headers::new().unwrap(); - for (k, v) in parts.headers.iter().map(|(k, v)| { - ( - k.as_str(), - v.to_str() - .unwrap_or_else(|_| panic!("Unparsable request header {}: {:?}", k.as_str(), v)), - ) - }) { - header_map.append(k, v).unwrap(); - } - // 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 |data: Option, status: u16, headers: Headers| { - let mut response_builder = Response::builder().status(status); - // convert `headers` to `Iterator` - 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 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); - }; - - 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); - #[allow(unused_must_use)] - { - 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 global: JsValue = js_sys::global().into(); - let promise = if Reflect::has(&global, &String::from("Window").into()).unwrap() { - Window::from(global).fetch_with_request_and_init(&request, &init) - } else if Reflect::has(&global, &String::from("WorkerGlobalScope").into()).unwrap() { - WorkerGlobalScope::from(global).fetch_with_request_and_init(&request, &init) - } else { - panic!("failed to get global context") - }; - let promise = promise.then(&closure_then).catch(&closure_catch); - sender.send(closure_then).unwrap(); - sender.send(closure_catch).unwrap(); - - FetchTask(Some(Handle { - active, - callbacks: receiver, - abort_controller, - promise, - })) -} - -impl Task for FetchTask { - fn is_active(&self) -> bool { - if let Some(ref task) = self.0 { - 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"); - - thread_local! { - static CATCH: Closure = Closure::wrap(Box::new(|_| ()) as Box); - } - handle.active.store(false, Ordering::SeqCst); - #[allow(unused_must_use)] - { - 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(); - } - } -}