diff --git a/Cargo.toml b/Cargo.toml index 5a037971..71d429f4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -18,14 +18,15 @@ serde = { version = "1", optional = true } serde_json = { version = "1", optional = true } clippy = { version = "0.0", optional = true } stdweb-derive = { path = "stdweb-derive" } +futures = { version = "0.1.18", optional = true } [dev-dependencies] serde_json = "1" serde_derive = "1" [features] -default = ["serde", "serde_json"] -dev = ["serde", "serde_json", "clippy"] +default = ["serde", "serde_json", "futures"] +dev = ["serde", "serde_json", "futures", "clippy"] serde-support = ["serde", "serde_json"] nightly = [] web_test = [] diff --git a/examples/promise/Cargo.toml b/examples/promise/Cargo.toml new file mode 100644 index 00000000..5ad4d05a --- /dev/null +++ b/examples/promise/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "promise" +version = "0.1.0" +authors = ["Pauan "] + +[dependencies] +stdweb = { path = "../.." } +futures = "0.1.18" \ No newline at end of file diff --git a/examples/promise/README.md b/examples/promise/README.md new file mode 100644 index 00000000..e69de29b diff --git a/examples/promise/src/main.rs b/examples/promise/src/main.rs new file mode 100644 index 00000000..793e0386 --- /dev/null +++ b/examples/promise/src/main.rs @@ -0,0 +1,244 @@ +#[macro_use] +extern crate stdweb; +extern crate futures; + +use futures::Future; +use stdweb::unstable::{TryInto}; +use stdweb::web::error::Error; +use stdweb::{Null, Promise, PromiseFuture}; +use std::rc::Rc; +use std::cell::RefCell; +use futures::{Poll, Async}; +use futures::task::{current, Task}; + + +fn log( a: &str ) { + js! { @(no_return) + console.log( @{a} ); + } +} + + +fn test_error_conversion() { + let a: PromiseFuture< Null, String > = js!( return Promise.reject( "hi!" ); ).try_into().unwrap(); + + PromiseFuture::spawn( + a.map( |_| () ).map_err( |x| { + log( &format!( "String error: {:#?}", x ) ); + } ) + ); + + let _a: PromiseFuture< Null, Error > = js!( return Promise.resolve( null ); ).try_into().unwrap(); + log( "Null works" ); + + let _a: PromiseFuture< Null, Error > = js!( return Promise.reject( new Error( "hi!" ) ); ).try_into().unwrap(); + log( "Error works" ); + + //let _a: PromiseFuture< Null, SyntaxError > = js!( return Promise.reject( new Error( "hi!" ) ); ).try_into().unwrap(); + //log( "Error conversion fails" ); +} + + +fn test_refcell() { + struct TaskA { + shared_state: Rc>, + task_b: Task, + } + + impl Future for TaskA { + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + js! { console.log("Poll TaskA"); } + + let foo = self.shared_state.borrow_mut(); + + js! { console.log(@{format!("TaskA 1: {:#?}", foo)}); } + + self.task_b.notify(); + + js! { console.log(@{format!("TaskA 2: {:#?}", foo)}); } + + Ok(Async::NotReady) + } + } + + struct TaskB { + shared_state: Rc>, + initialized: bool, + } + + impl Future for TaskB { + type Item = (); + type Error = (); + + fn poll(&mut self) -> Poll { + js! { console.log("Poll TaskB"); } + + if !self.initialized { + self.initialized = true; + + let task_b = current(); + + let foo = self.shared_state.borrow(); + + js! { console.log(@{format!("TaskB 1: {:#?}", foo)}); } + + PromiseFuture::spawn(TaskA { + shared_state: self.shared_state.clone(), + task_b: task_b, + }); + } + + let foo = self.shared_state.borrow(); + + js! { console.log(@{format!("TaskB 1: {:#?}", foo)}); } + + Ok(Async::NotReady) + } + } + + PromiseFuture::spawn(TaskB { + shared_state: Rc::new(RefCell::new(0)), + initialized: false + }); +} + + +fn test_panic() { + let promise: Promise = js!( return Promise.resolve(null); ).try_into().unwrap(); + + promise.done( |result: Result< Null, Error >| { + log( &format!( "Promise result: {:#?}", result ) ); + panic!( "Testing panic!" ); + } ); +} + + +fn test_notify() { + struct MyFuture { + polls: u32, + count: u32, + done: bool, + receiver: futures::unsync::oneshot::Receiver< () >, + } + + impl MyFuture { + fn new( count: u32 ) -> Self { + let ( sender, receiver ) = futures::unsync::oneshot::channel(); + + let callback = || { + log( "setTimeout done" ); + + log( &format!("Sending {:#?}", sender.send( () ) ) ); + }; + + log( "setTimeout started" ); + + js! { @(no_return) + setTimeout( function () { + @{stdweb::Once( callback )}(); + }, 1000 ); + } + + Self { + polls: 0, + count: count, + done: false, + receiver, + } + } + } + + impl Future for MyFuture { + type Item = u32; + type Error = (); + + fn poll( &mut self ) -> futures::Poll< Self::Item, Self::Error > { + self.polls += 1; + + if !self.done { + match self.receiver.poll() { + Ok( futures::Async::Ready( () ) ) => self.done = true, + + Ok( futures::Async::NotReady ) => {}, + + Err( _ ) => self.done = true, + } + } + + if self.done { + if self.count == 0 { + Ok( futures::Async::Ready( self.polls ) ) + + } else { + self.count -= 1; + + let task = futures::task::current(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + task.notify(); + + Ok( futures::Async::NotReady ) + } + + } else { + Ok( futures::Async::NotReady ) + } + } + } + + PromiseFuture::spawn( + MyFuture::new( 5 ).map( |x| { + log( &format!( "MyFuture count: {}", x ) ); + assert_eq!( x, 7 ); + } ) + ); +} + + +fn test_timeout() { + fn sleep( ms: u32 ) -> PromiseFuture< Null, Error > { + js!( return new Promise( function ( success, failure ) { + setTimeout( function () { + success( null ); + }, @{ms} ); + } ); ).try_into().unwrap() + } + + PromiseFuture::spawn( + sleep( 2000 ).inspect( |_| log( "Timeout 1 done!") ).join( + sleep( 2000 ).inspect( |_| log( "Timeout 2 done!" ) ) ) + .and_then( |_| + sleep( 1000 ).inspect( |_| log( "Timeout 3 done!") ) ) + .and_then( |_| + futures::future::err( Error::new( "Testing error!" ) ) ) + .map_err( |e| e.print() ) + ); +} + + +fn main() { + stdweb::initialize(); + + test_refcell(); + test_panic(); + test_notify(); + test_timeout(); + test_error_conversion(); + + stdweb::event_loop(); +} + diff --git a/src/lib.rs b/src/lib.rs index c8d58981..37cc6862 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -94,6 +94,9 @@ extern crate stdweb_internal_macros; #[cfg(all(target_arch = "wasm32", target_os = "unknown"))] pub use stdweb_internal_macros::js_export; +#[cfg(feature = "futures")] +extern crate futures; + #[macro_use] extern crate stdweb_derive; @@ -128,6 +131,11 @@ pub use webcore::instance_of::InstanceOf; pub use webcore::reference_type::ReferenceType; pub use webcore::serialization::JsSerialize; +pub use webcore::promise::Promise; + +#[cfg(feature = "futures")] +pub use webcore::promise_future::PromiseFuture; + #[cfg(feature = "serde")] /// A module with serde-related APIs. pub mod serde { diff --git a/src/webapi/error.rs b/src/webapi/error.rs index 9b7ff295..546399bf 100644 --- a/src/webapi/error.rs +++ b/src/webapi/error.rs @@ -36,6 +36,26 @@ pub trait IError: ReferenceType { #[reference(instance_of = "Error")] pub struct Error( Reference ); +impl Error { + /// Creates a new `Error` with the specified `description`. + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Error) + #[inline] + pub fn new( description: &str ) -> Self { + js!( return new Error( @{description} ); ).try_into().unwrap() + } + + /// Prints the `Error` to the console (this prints the error's description and also its stack trace). + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) + #[inline] + pub fn print( &self ) { + js! { @(no_return) + console.error( @{self} ); + } + } +} + // Error specification: // https://www.ecma-international.org/ecma-262/6.0/#sec-error-objects diff --git a/src/webcore/mod.rs b/src/webcore/mod.rs index 5b989657..fcfa525b 100644 --- a/src/webcore/mod.rs +++ b/src/webcore/mod.rs @@ -15,6 +15,13 @@ pub mod unsafe_typed_array; pub mod once; pub mod instance_of; pub mod reference_type; +pub mod promise; + +#[cfg(feature = "futures")] +pub mod promise_future; + +#[cfg(feature = "futures")] +pub mod promise_executor; #[cfg(feature = "nightly")] pub mod void { diff --git a/src/webcore/promise.rs b/src/webcore/promise.rs new file mode 100644 index 00000000..075c2883 --- /dev/null +++ b/src/webcore/promise.rs @@ -0,0 +1,163 @@ +use std; +use webcore::once::Once; +use webcore::value::{Value, Reference}; +use webcore::try_from::{TryInto, TryFrom}; + +#[cfg(feature = "futures")] +use futures::unsync::oneshot::channel; + +#[cfg(feature = "futures")] +use super::promise_future::PromiseFuture; + + +/// A `Promise` object represents the eventual completion (or failure) of an asynchronous operation, and its resulting value. +/// +/// In most situations you shouldn't use this, use [`PromiseFuture`](struct.PromiseFuture.html) instead. +/// +/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise) +// https://www.ecma-international.org/ecma-262/6.0/#sec-promise-objects +#[derive(Clone, Debug, ReferenceType)] +#[reference(instance_of = "Promise")] +pub struct Promise( Reference ); + +impl Promise { + // https://www.ecma-international.org/ecma-262/6.0/#sec-promise-resolve-functions + fn is_thenable( input: &Value ) -> bool { + (js! { + var input = @{input}; + // This emulates the `Type(input) is Object` and `IsCallable(input.then)` ECMAScript abstract operations. + return Object( input ) === input && + typeof input.then === "function"; + }).try_into().unwrap() + } + + /// This function should rarely be needed, use [`PromiseFuture`](struct.PromiseFuture.html) instead. + /// + /// This function is needed if you have a JavaScript value which is a Promise-like object + /// (it has a `then` method) but it isn't a true `Promise`. + /// + /// That situation is rare, but it can happen if you are using a Promise library such as jQuery or + /// Bluebird. + /// + /// In that situation you can use `Promise::from_thenable(value)` to convert it into a true `Promise`. + /// + /// If the `input` isn't a Promise-like object then it returns `None`. + /// + /// # Examples + /// + /// Convert a Promise-like object to a `Promise`: + /// + /// ```rust + /// // jQuery Promise + /// Promise::from_thenable(js!( return $.get("test.php"); )) + /// + /// // Bluebird Promise + /// Promise::from_thenable(js!( return bluebird_promise.timeout(1000); )) + /// ``` + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve) + // https://www.ecma-international.org/ecma-262/6.0/#sec-promise.resolve + // https://www.ecma-international.org/ecma-262/6.0/#sec-promise-resolve-functions + // https://www.ecma-international.org/ecma-262/6.0/#sec-promiseresolvethenablejob + // TODO change this later to use &Reference + pub fn from_thenable( input: Value ) -> Option< Self > { + // TODO this can probably be made more efficient + if Promise::is_thenable( &input ) { + Some( js!( return Promise.resolve( @{input} ); ).try_into().unwrap() ) + + } else { + None + } + } + + /// This method is usually not needed, use [`PromiseFuture`](struct.PromiseFuture.html) instead. + /// + /// When the `Promise` either succeeds or fails, it calls the `callback` with the result. + /// + /// It does not wait for the `Promise` to succeed / fail (it does not block the thread). + /// + /// The `callback` is guaranteed to be called asynchronously even if the `Promise` is already succeeded / failed. + /// + /// If the `Promise` never succeeds / fails then the `callback` will never be called, and it will leak memory. + /// + /// # Examples + /// + /// ```rust + /// promise.done(|result| { + /// match result { + /// Ok(success) => { ... }, + /// Err(error) => { ... }, + /// } + /// }); + /// ``` + /// + /// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/then) + // https://www.ecma-international.org/ecma-262/6.0/#sec-performpromisethen + pub fn done< A, B, F >( &self, callback: F ) + where A: TryFrom< Value >, + B: TryFrom< Value >, + // TODO these Debug constraints are only needed because of unwrap + A::Error: std::fmt::Debug, + B::Error: std::fmt::Debug, + F: FnOnce( Result< A, B > ) + 'static { + + let callback = |value: Value, success: bool| { + let value: Result< A, B > = if success { + // TODO figure out a way to avoid the unwrap + let value: A = value.try_into().unwrap(); + Ok( value ) + } else { + // TODO figure out a way to avoid the unwrap + let value: B = value.try_into().unwrap(); + Err( value ) + }; + + callback( value ); + }; + + js! { @(no_return) + var callback = @{Once( callback )}; + + // TODO don't swallow any errors thrown inside callback + @{self}.then( function ( value ) { + callback( value, true ); + }, function ( value ) { + callback( value, false ); + } ); + } + } + + /// This method should rarely be needed, instead use [`value.try_into()`](unstable/trait.TryInto.html) to convert directly from a [`Value`](enum.Value.html) into a [`PromiseFuture`](struct.PromiseFuture.html). + /// + /// This method converts the `Promise` into a [`PromiseFuture`](struct.PromiseFuture.html), so that it can be used as a Rust [`Future`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html). + /// + /// # Examples + /// + /// ```rust + /// promise.to_future().map(|x| x + 1) + /// ``` + // We can't use the IntoFuture trait because Promise doesn't have a type argument + // TODO explain more why we can't use the IntoFuture trait + #[cfg(feature = "futures")] + pub fn to_future< A, B >( &self ) -> PromiseFuture< A, B > + where A: TryFrom< Value > + 'static, + B: TryFrom< Value > + 'static, + // TODO remove these later + A::Error: std::fmt::Debug, + B::Error: std::fmt::Debug { + + let ( sender, receiver ) = channel(); + + self.done( |value| { + // TODO is this correct ? + match sender.send( value ) { + Ok( _ ) => {}, + Err( _ ) => {}, + }; + } ); + + PromiseFuture { + future: receiver, + } + } +} diff --git a/src/webcore/promise_executor.rs b/src/webcore/promise_executor.rs new file mode 100644 index 00000000..ba093417 --- /dev/null +++ b/src/webcore/promise_executor.rs @@ -0,0 +1,93 @@ +use futures::future::{Future, ExecuteError, Executor}; +use futures::executor::{self, Notify, Spawn}; +use futures::Async; +use std::result::Result as StdResult; +use std::cell::{Cell, RefCell}; +use std::rc::Rc; +use webcore::once::Once; + +// This functionality should really be in libstd, because the implementation +// looks stupid. +unsafe fn clone_raw< T >( ptr: *const T ) -> Rc< T > { + let result = Rc::from_raw( ptr ); + ::std::mem::forget( result.clone() ); + result +} + +// Typing this out is tedious +type BoxedFuture = Box< Future< Item = (), Error = () > + 'static >; + +struct SpawnedTask { + is_queued: Cell< bool >, + spawn: RefCell< Option< Spawn< BoxedFuture > > >, +} + +impl SpawnedTask { + fn new< F >( future: F ) -> Rc< Self > + where F: Future< Item = (), Error = () > + 'static { + Rc::new( Self { + is_queued: Cell::new( false ), + spawn: RefCell::new( Some( executor::spawn( + Box::new( future ) as BoxedFuture + ) ) ), + } ) + } + + fn poll( &self ) { + let mut spawn = self.spawn.borrow_mut(); + + // Take the future so that if we panic it gets dropped + if let Some( mut spawn_future ) = spawn.take() { + // Clear `is_queued` flag + self.is_queued.set( false ); + + if spawn_future.poll_future_notify( &&Core, self as *const _ as usize ) == Ok( Async::NotReady ) { + // Future was not ready, so put it back + *spawn = Some( spawn_future ); + } + } + } + + fn notify( spawned: Rc< SpawnedTask > ) { + // If not already queued + if !spawned.is_queued.replace( true ) { + // Poll on next micro-task + js! { @(no_return) + Promise.resolve().then( function () { + @{Once( move || spawned.poll() )}(); + } ); + } + } + } +} + +struct Core; + +impl< F > Executor< F > for Core where + F: Future< Item = (), Error = () > + 'static { + fn execute( &self, future: F ) -> StdResult< (), ExecuteError< F > > { + SpawnedTask::notify( SpawnedTask::new( future ) ); + Ok( () ) + } +} + +impl Notify for Core { + fn notify( &self, spawned_id: usize ) { + SpawnedTask::notify( unsafe { clone_raw( spawned_id as *const _ ) } ) + } + + fn clone_id( &self, id: usize ) -> usize { + unsafe { Rc::into_raw( clone_raw( id as *const SpawnedTask ) ) as usize } + } + + fn drop_id( &self, id: usize ) { + unsafe { Rc::from_raw( id as *const SpawnedTask ) }; + } +} + + +#[inline] +pub fn spawn< F >( future: F ) where + F: Future< Item = (), Error = () > + 'static { + Core.execute( future ).unwrap(); +} diff --git a/src/webcore/promise_future.rs b/src/webcore/promise_future.rs new file mode 100644 index 00000000..6b4801c9 --- /dev/null +++ b/src/webcore/promise_future.rs @@ -0,0 +1,118 @@ +use std; +use webcore::value::{Value, ConversionError}; +use webcore::try_from::{TryInto, TryFrom}; +use futures::{Future, Poll, Async}; +use futures::unsync::oneshot::Receiver; +use webcore::promise_executor::spawn; +use super::promise::Promise; + + +/// This allows you to use a JavaScript [`Promise`](struct.Promise.html) as if it is a Rust [`Future`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html). +/// +/// The preferred way to create a `PromiseFuture` is to use [`value.try_into()`](unstable/trait.TryInto.html) on a JavaScript [`Value`](enum.Value.html). +/// +/// # Examples +/// +/// Convert a JavaScript `Promise` into a `PromiseFuture`: +/// +/// ```rust +/// use stdweb::PromiseFuture; +/// use stdweb::web::error::Error; +/// +/// let future: PromiseFuture = js!( return Promise.resolve("foo"); ).try_into().unwrap(); +/// ``` +pub struct PromiseFuture< A, B > { + pub(crate) future: Receiver< Result< A, B > >, +} + +impl PromiseFuture< (), () > { + /// Asynchronously runs the [`Future`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html) and then immediately returns. + /// This does not block the current thread. The only way to retrieve the value of the future is to use the various + /// [`Future`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html) methods, such as + /// [`map`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html#method.map) or + /// [`inspect`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html#method.inspect). + /// + /// This function requires you to handle all errors yourself. Because the errors happen asynchronously, the only way to catch them is + /// to use a [`Future`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html) method, such as + /// [`map_err`](https://docs.rs/futures/0.1.18/futures/future/trait.Future.html#method.map_err). + /// + /// It is very common to want to print the errors to the console. You can do that by using `.map_err(|e| e.print())` + /// + /// This function is normally called once in `main`, it is usually not needed to call it multiple times. + /// + /// # Examples + /// + /// Asynchronously run a future in `main`, printing any errors to the console: + /// + /// ```rust + /// fn main() { + /// PromiseFuture::spawn( + /// create_some_future() + /// .map_err(|e| e.print()) + /// ); + /// } + /// ``` + /// + /// Inspect the output value of the future: + /// + /// ```rust + /// fn main() { + /// PromiseFuture::spawn( + /// create_some_future() + /// .inspect(|x| println!("Future finished: {:#?}", x)) + /// .map_err(|e| e.print()) + /// ); + /// } + /// ``` + /// + /// Catch errors and handle them yourself: + /// + /// ```rust + /// fn main() { + /// PromiseFuture::spawn( + /// create_some_future() + /// .map_err(|e| handle_error_somehow(e)) + /// ); + /// } + /// ``` + #[inline] + pub fn spawn< B >( future: B ) where + B: Future< Item = (), Error = () > + 'static { + spawn( future ); + } +} + +impl< A, B > std::fmt::Debug for PromiseFuture< A, B > { + fn fmt( &self, formatter: &mut std::fmt::Formatter ) -> std::fmt::Result { + write!( formatter, "PromiseFuture" ) + } +} + +impl< A, B > Future for PromiseFuture< A, B > { + type Item = A; + type Error = B; + + fn poll( &mut self ) -> Poll< Self::Item, Self::Error > { + // TODO maybe remove this unwrap ? + match self.future.poll().unwrap() { + Async::Ready( Ok( a ) ) => Ok( Async::Ready( a ) ), + Async::Ready( Err( e ) ) => Err( e ), + Async::NotReady => Ok( Async::NotReady ), + } + } +} + +impl< A, B > TryFrom< Value > for PromiseFuture< A, B > + where A: TryFrom< Value > + 'static, + B: TryFrom< Value > + 'static, + // TODO remove this later + A::Error: std::fmt::Debug, + B::Error: std::fmt::Debug { + + type Error = ConversionError; + + fn try_from( v: Value ) -> Result< Self, Self::Error > { + let promise: Promise = v.try_into()?; + Ok( promise.to_future() ) + } +}