#![cfg_attr(test, feature(linkage))]
pub mod jsbox;
use stdweb::unstable::TryFrom;
use std::convert::Infallible;
use std::marker::PhantomData;
use std::mem::drop;
use std::panic;
use stdweb::{self, js, console};
use crate::jsbox::*;
#[derive(Clone)]
pub struct RawPromise {
p: stdweb::Reference,
}
impl RawPromise {
pub fn from_reference(p: stdweb::Reference) -> Self {
Self { p }
}
pub fn from_value(v: stdweb::Value) -> Self {
Self { p: v.into_reference().expect("Value passed to RawPromise::from_value was not an object") }
}
pub fn js_obj(&self) -> &stdweb::Reference {
&self.p
}
pub fn new_resolved(v: stdweb::Value) -> Self {
let p = js!{ return Promise.resolve(@{v}); };
Self::from_reference(p.into_reference().expect("Promise.resolve did not return an object"))
}
pub fn new_rejected(v: stdweb::Value) -> Self {
let p = js!{ return Promise.reject(@{v}); };
Self::from_reference(p.into_reference().expect("Promise.resolve did not return an object"))
}
pub fn from_result(v: Result<stdweb::Value, stdweb::Value>) -> Self {
match v {
Ok(v) => Self::new_resolved(v),
Err(v) => Self::new_rejected(v),
}
}
pub fn then<F: FnOnce(Result<stdweb::Value, stdweb::Value>) -> stdweb::Value + Send + 'static>(&self, f: F) -> Self {
let invoke_cb = move |v: stdweb::Value, is_ok: bool| {
let v_res = if is_ok { Ok(v) } else { Err(v) };
return invoke_tried(|| f(v_res));
};
let promise_val = js!{
let invoke_cb = @{stdweb::Once(invoke_cb)};
return @{self.js_obj()}.then(function(v) {
return invoke_cb(v, true);
}, function(v) {
return invoke_cb(v, false);
})
};
let promise_ref = promise_val.into_reference().expect("promise.then did not return an object");
Self{p: promise_ref}
}
pub fn then_to_typed<TOk: Send, TErr: Send, F: FnOnce(Result<stdweb::Value, stdweb::Value>) -> PromiseResult<TOk, TErr> + Send + 'static>(&self, f: F) -> Promise<TOk, TErr> {
let cb = move |v: Result<stdweb::Value, stdweb::Value>| {
invoke_tried(|| promise_result_to_value(f(v)))
};
Promise {
p: self.then(cb),
has_consumer: false,
_ph: Default::default()
}
}
pub fn all<I: IntoIterator<Item=RawPromise>>(promises: I) -> RawPromise {
let promise_objs = promises.into_iter()
.map(|promise| promise.p)
.collect::<Vec<_>>();
let promise_val = js!{
return Promise.all(@{promise_objs});
};
Self::from_value(promise_val)
}
pub fn all_settled<I: IntoIterator<Item=RawPromise>>(promises: I) -> RawPromise {
let promise_objs = promises.into_iter()
.map(|promise| promise.p)
.collect::<Vec<_>>();
let promise_val = js!{
return Promise.allSettled(@{promise_objs});
};
Self::from_value(promise_val)
}
pub fn race<I: IntoIterator<Item=RawPromise>>(promises: I) -> RawPromise {
let promise_objs = promises.into_iter()
.map(|promise| promise.p)
.collect::<Vec<_>>();
let promise_val = js!{
return Promise.race(@{promise_objs});
};
Self::from_value(promise_val)
}
}
pub struct Promise<TOk: Send+'static, TErr: Send+'static> {
p: RawPromise,
has_consumer: bool,
_ph: PhantomData<fn(TOk, TErr)>,
}
impl<TOk: Send+'static, TErr: Send+'static> Promise<TOk, TErr> {
pub fn then<TNewOk: Send+'static, TNewErr: Send+'static, F: FnOnce(Result<TOk, TErr>) -> PromiseResult<TNewOk, TNewErr> + Send + 'static>(mut self, f: F) -> Promise<TNewOk, TNewErr> {
let cb = move |res: Result<stdweb::Value, stdweb::Value>| {
let unboxed_res = match res {
Ok(raw_v) => {
if raw_v == stdweb::Value::Null {
return stdweb::Value::Null;
}
let unboxed_maybe = unsafe { js_unbox::<TOk>(raw_v.clone()) };
let unboxed = match unboxed_maybe {
Some(v) => v,
None => {
console!(error, "Could not unbox rust value from promise result. This is probably a bug with the js-promises crate. Value:", raw_v);
return stdweb::Value::Null;
}
};
Ok(unboxed)
},
Err(raw_v) => {
if raw_v == stdweb::Value::Null {
return stdweb::Value::Null;
}
let unboxed_maybe = unsafe { js_unbox::<TErr>(raw_v.clone()) };
let unboxed = match unboxed_maybe {
Some(v) => v,
None => {
console!(error, "Could not unbox rust value from promise result. This is probably a bug with the js-promises crate. Value:", raw_v);
return stdweb::Value::Null;
}
};
Err(unboxed)
},
};
return invoke_tried(|| promise_result_to_value(f(unboxed_res)));
};
self.has_consumer = true;
Promise {
p: self.p.then(cb),
has_consumer: false,
_ph: Default::default(),
}
}
pub fn new_resolved(v: TOk) -> Self {
Self {
p: RawPromise::new_resolved(js_box(v)),
has_consumer: false,
_ph: Default::default(),
}
}
pub fn new_rejected(v: TErr) -> Self {
Self {
p: RawPromise::new_rejected(js_box(v)),
has_consumer: false,
_ph: Default::default(),
}
}
pub fn from_result(v: Result<TOk, TErr>) -> Self {
match v {
Ok(v) => Self::new_resolved(v),
Err(v) => Self::new_rejected(v),
}
}
pub fn then_to_raw<F: FnOnce(Result<TOk, TErr>) -> stdweb::Value + Send + 'static>(mut self, f: F) -> RawPromise {
let cb = move |v: Result<stdweb::Value, stdweb::Value>| {
let unboxed_res = match v {
Ok(v) => unsafe { Ok(js_unbox::<TOk>(v).expect("Promise::then got invalid box to cb")) },
Err(v) => unsafe { Err(js_unbox::<TErr>(v).expect("Promise::then got invalid box to cb")) },
};
return (f)(unboxed_res);
};
self.has_consumer = true;
self.p.then(cb)
}
pub fn map<TNewOk: Send+'static, F: FnOnce(TOk) -> TNewOk + Send + 'static>(self, f: F) -> Promise<TNewOk, TErr> {
self.then(move |v: Result<TOk, TErr>| v.map(f).map(|v| v.into()))
}
pub fn map_err<TNewErr: Send+'static, F: FnOnce(TErr) -> TNewErr + Send + 'static>(self, f: F) -> Promise<TOk, TNewErr> {
self.then(move |v: Result<TOk, TErr>| v.map_err(f).map(|v| v.into()))
}
pub fn and_then<TNewOk: Send+'static, F: FnOnce(TOk) -> PromiseResult<TNewOk, TErr> + Send + 'static>(self, f: F) -> Promise<TNewOk, TErr> {
self.then(move |v: Result<TOk, TErr>| {
let v = match v {
Ok(v) => v,
Err(err) => { return Err(err).into(); },
};
(f)(v)
})
}
pub fn all_settled<I: IntoIterator<Item=Self>>(promises: I) -> Promise<Vec<Result<TOk, TErr>>, Infallible> {
let promise_objs = promises.into_iter()
.map(|mut promise| {
promise.has_consumer = true;
promise.p.js_obj().clone()
})
.collect::<Vec<_>>();
RawPromise::from_value(js!{
return Promise.allSettled(@{promise_objs});
}).then_to_typed(|res| {
let array = stdweb::Array::try_from(
res
.expect("Promise from Promise.allSettled was rejected")
.into_reference()
.expect("Value from Promise.allSettled was not a reference")
).expect("Value from Promise.allSettled was not an array");
let value_vec = Vec::from(array);
Ok(value_vec.into_iter()
.map(|val| {
let status_val = js!{ return @{val.clone()}.status; };
let status = status_val.as_str().expect("Promise.allSettled entry.status was not a string");
match status {
"fulfilled" => {
let v = js!{ return @{val}.value; };
unsafe { Ok(js_unbox::<TOk>(v).expect("Promise.allSettled got invalid box to cb")) }
},
"rejected" => unsafe {
let v = js!{ return @{val}.reason; };
Err(js_unbox::<TErr>(v).expect("Promise.allSettled got invalid box to cb"))
},
_ => { panic!("Promise.allSettled entry.status was not 'fulfilled' or 'rejected', was {:?}", status); }
}
})
.collect::<Vec<_>>()
.into()
)
})
}
}
impl<TOk: Send, TErr: Send> Drop for Promise<TOk, TErr> {
fn drop(&mut self) {
if self.has_consumer {
return;
}
self.p.then(|v: Result<stdweb::Value, stdweb::Value>| {
match v {
Ok(v) => drop(unsafe { js_unbox::<TOk>(v) }),
Err(v) => drop(unsafe { js_unbox::<TErr>(v) }),
}
return stdweb::Value::Null;
});
}
}
pub type PromiseResult<TOk, TErr> = Result<PromiseOk<TOk, TErr>, TErr>;
pub enum PromiseOk<TOk: Send+'static, TErr: Send+'static> {
Immediate(TOk),
Promise(Promise<TOk, TErr>),
}
impl<TOk: Send, TErr: Send> From<TOk> for PromiseOk<TOk, TErr> {
fn from(v: TOk) -> Self {
Self::Immediate(v)
}
}
impl<TOk: Send, TErr: Send> From<Promise<TOk, TErr>> for PromiseOk<TOk, TErr> {
fn from(v: Promise<TOk, TErr>) -> Self {
Self::Promise(v)
}
}
fn promise_result_to_value<TOk: Send, TErr: Send>(v: PromiseResult<TOk, TErr>) -> stdweb::Value {
match v {
Ok(PromiseOk::Immediate(v)) => js!{ return Promise.resolve(@{js_box(v)}) },
Ok(PromiseOk::Promise(mut p)) => {
p.has_consumer = true;
p.p.js_obj().into()
},
Err(v) => js!{ return Promise.reject(@{js_box(v)}) },
}
}
fn invoke_tried<F: FnOnce() -> stdweb::Value + 'static>(f: F) -> stdweb::Value {
return js!{
try {
return @{stdweb::Once(f)}();
} catch(e) {
console.error("Rust future callback panicked or threw JS error:", e);
return null;
}
};
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json;
use std::fmt::Display;
use std::string::ToString;
use stdweb::async_test;
use stdweb::console;
fn test_promise<TOk: Send, TErr: Send, FDone>(p: Promise<TOk, TErr>, done: FDone)
where
TErr: Display,
FDone: FnOnce(Result<(), String>) + Send + 'static,
{
p.then_to_raw(move |res| {
let res = res
.map(|_| ())
.map_err(|e| format!("Promise rejected: {}", e));
done(res);
return stdweb::Value::Null;
});
}
fn test_raw_promise<FDone>(p: RawPromise, done: FDone)
where
FDone: FnOnce(Result<(), String>) + Send + 'static,
{
p.then(move |res| {
let res = res
.map(|_| ())
.map_err(|e| format!("Promise rejected with value {}", js_to_string(e)));
done(res);
return stdweb::Value::Null;
});
}
fn reject_raw<Why: ToString>(why: Why) -> stdweb::Value {
return js!{ return Promise.reject(@{why.to_string()}); };
}
fn js_to_string(v: stdweb::Value) -> String {
return js!{ return JSON.stringify(@{v}); }.into_string().unwrap();
}
#[async_test]
fn raw_promise_new_resolved<F: FnOnce(Result<(), String>)>(done: F) {
let p = RawPromise::new_resolved(123u32.into())
.then(|res| {
let value = match res {
Ok(v) => v,
Err(_) => {
return reject_raw("new_resolved was rejected");
}
};
if value != stdweb::Value::Number(123u32.into()) {
return reject_raw(format!("new_resolved expected 123, got {}", js_to_string(value)));
}
return stdweb::Value::Null;
});
test_raw_promise(p, done);
}
#[async_test]
fn raw_promise_new_rejected<F: FnOnce(Result<(), String>)>(done: F) {
let p = RawPromise::new_rejected("oof".into())
.then(|res| {
let reason = match res {
Ok(_) => {
return reject_raw("new_rejected was resolved");
},
Err(v) => v,
};
if reason.as_str() != Some("oof") {
return reject_raw(format!("new_rejected expected 'oof', got {}", js_to_string(reason)));
}
return stdweb::Value::Null;
});
test_raw_promise(p, done);
}
#[async_test]
fn promise_new_resolved<F: FnOnce(Result<(), String>)>(done: F) {
let p = Promise::<String, String>::new_resolved("hello world".into())
.then(|res| {
let value: String = match res {
Ok(v) => v,
Err(_) => {
return Err(String::from("new_resolved was rejected"));
}
};
if value.as_str() != "hello world" {
return Err(format!("new_resolved expected 'hello world', got {:?}", value));
}
return Ok(PromiseOk::Immediate(()));
});
test_promise(p, done);
}
#[async_test]
fn promise_new_rejected<F: FnOnce(Result<(), String>)>(done: F) {
let p = Promise::<String, String>::new_rejected("oof".into())
.then(|res| {
let value: String = match res {
Err(v) => v,
Ok(_) => {
return Err(String::from("new_rejected was resolved"));
}
};
if value.as_str() != "oof" {
return Err(format!("new_rejected expected 'oof', got {:?}", value));
}
return Ok(PromiseOk::Immediate(()));
});
test_promise(p, done);
}
fn http_req(url: &str) -> Promise<serde_json::Value,String> {
let req = stdweb::web::XmlHttpRequest::new();
req.open("GET", url).expect("open failed");
let p = RawPromise::from_value(js!{
return new Promise(function(resolve, reject) {
let req = @{req.as_ref().clone()};
req.addEventListener("load", resolve);
req.addEventListener("abort", function() { reject("abort"); });
req.addEventListener("error", function() { reject("error"); });
req.addEventListener("timeout", function() { reject("timeout"); });
});
});
req.send().unwrap();
let p = p.then_to_typed::<serde_json::Value, String, _>(move |res| {
let _ev = res.map_err(|v| v.into_string().unwrap())?;
let body = req.response_text().unwrap().unwrap();
let out = serde_json::from_str::<serde_json::Value>(&body).unwrap();
return Ok(PromiseOk::Immediate(out));
});
return p;
}
#[async_test]
fn ajax_request<F: FnOnce(Result<(), String>)>(done: F) {
let p = http_req("https://httpbin.org/get")
.and_then(|v| {
let url_v = v.pointer("/url").ok_or(String::from("Expected url field in response"))?;
let url = url_v.as_str().ok_or(String::from("url field in response was not a string"))?;
if url == "https://httpbin.org/get" {
return Ok(PromiseOk::Immediate(()));
} else {
return Err(format!("url field in response was incorrect, had value {:?}", url));
}
});
test_promise(p, done);
}
#[async_test]
fn return_a_promise<F: FnOnce(Result<(), String>)>(done: F) {
let p = Promise::<String,String>::new_resolved(String::from("https://httpbin.org/get"))
.and_then(|url| {
console!(log, "url:", &url);
return Ok(PromiseOk::Promise(http_req(&url)));
})
.and_then(|v| {
let url_v = v.pointer("/url").ok_or(String::from("Expected url field in response"))?;
let url = url_v.as_str().ok_or(String::from("url field in response was not a string"))?;
if url == "https://httpbin.org/get" {
return Ok(PromiseOk::Immediate(()));
} else {
return Err(format!("url field in response was incorrect, had value {:?}", url));
}
});
test_promise(p, done);
}
}