use super::Task;
use crate::callback::Callback;
use crate::format::{Binary, Format, Text};
use failure::Fail;
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};
pub use http::{HeaderMap, Method, Request, Response, StatusCode, Uri};
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Cache {
#[serde(rename = "default")]
DefaultCache,
NoStore,
Reload,
NoCache,
ForceCache,
OnlyIfCached,
}
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Credentials {
Omit,
Include,
SameOrigin,
}
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
SameOrigin,
NoCors,
Cors,
}
#[derive(Serialize)]
#[serde(rename_all = "kebab-case")]
pub enum Redirect {
Follow,
Error,
Manual,
}
#[derive(Serialize, Default)]
pub struct FetchOptions {
#[serde(skip_serializing_if = "Option::is_none")]
pub cache: Option<Cache>,
#[serde(skip_serializing_if = "Option::is_none")]
pub credentials: Option<Credentials>,
#[serde(skip_serializing_if = "Option::is_none")]
pub redirect: Option<Redirect>,
#[serde(skip_serializing_if = "Option::is_none")]
pub mode: Option<Mode>,
}
#[derive(Debug, Fail)]
enum FetchError {
#[fail(display = "failed response")]
FailedResponse,
}
#[must_use]
pub struct FetchTask(Option<Value>);
#[derive(Default)]
pub struct FetchService {}
impl FetchService {
pub fn new() -> Self {
Self {}
}
pub fn fetch<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Text>,
OUT: From<Text>,
{
fetch_impl::<IN, OUT, String, String>(false, request, None, callback)
}
pub fn fetch_with_options<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
options: FetchOptions,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Text>,
OUT: From<Text>,
{
fetch_impl::<IN, OUT, String, String>(false, request, Some(options), callback)
}
pub fn fetch_binary<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Binary>,
OUT: From<Binary>,
{
fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, None, callback)
}
pub fn fetch_binary_with_options<IN, OUT: 'static>(
&mut self,
request: Request<IN>,
options: FetchOptions,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Binary>,
OUT: From<Binary>,
{
fetch_impl::<IN, OUT, Vec<u8>, ArrayBuffer>(true, request, Some(options), callback)
}
}
fn fetch_impl<IN, OUT: 'static, T, X>(
binary: bool,
request: Request<IN>,
options: Option<FetchOptions>,
callback: Callback<Response<OUT>>,
) -> FetchTask
where
IN: Into<Format<T>>,
OUT: From<Format<T>>,
T: JsSerialize,
X: TryFrom<Value> + Into<T>,
{
let (parts, body) = request.into_parts();
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 uri = format!("{}", parts.uri);
let method = parts.method.as_str();
let body = body.into().ok();
let callback = move |success: bool, status: u16, headers: HashMap<String, String>, data: X| {
let mut response_builder = Response::builder();
response_builder.status(status);
for (key, values) in &headers {
response_builder.header(key.as_str(), values.as_str());
}
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);
};
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) {
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();
}
}
}