-
Notifications
You must be signed in to change notification settings - Fork 177
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Adding in Promises #103
Adding in Promises #103
Conversation
There are a few problems here:
Before jumping into implementing a promises -> futures adaptor, I think it would be useful to implement a futures-compatible reactor on top of the javascript event loop, that way it's possible to test that everything is working. |
I have no idea what you're talking about. It uses
Yup! That'll need to be added.
Yes it is needed. You can't use
That's a good idea, I'll do that.
Indeed, which is why I said this is untested and not ready to merge. It is a work in progress. |
Ah yes
You can just store the |
You're right that is possible, but I was concerned about potential performance issues, since it would be rebuilding the Future every time As for not using combinators at all, yes that is possible, but it would require A good example of not using the combinators would be implementing |
What? No, take a look at how the future combinators are actually implemented, you would just do this: fn poll( &mut self ) -> Poll< Self::Item, Self::Error > {
self.future.poll().map_err( |x| JSError::new( x.description() ) ).and_then( future::result )
} There's no performance penalty here: this is exactly how
I think you are misunderstanding the terminology - combinators are the methods used to compose futures together, eg. Not using |
Yes I know what you were referring to.
I will verify that the performance of the compiled binary code will be just as fast (or faster). I don't judge performance based upon source code.
I am not misunderstanding anything, I know exactly what "combinator" means in this context.
You are using the combinators within the implementation of |
Alright, so I'm not quite sure why we are boxing the oneshot channel's receiver, as there should be absolutely no reason for doing so. The futures I have in stdweb-io don't do any of this either. Additionally you lose all the OIBITs if you use trait objects, so I wouldn't use them unless there's a good reason for them, which I don't quite see yet. Regardless, most of this PR looks pretty good to me. This is still missing an Executor. You can take a look at the one I made, or I can make a PR after this that brings the Executor. |
Yes, I definitely don't like using
Sure, I'll take a deeper look at your code, I'll let you know if I can't figure it out. @Diggsey I understand now what you meant about "using the combinators on However, I tried moving the combinators into error[E0631]: type mismatch in function arguments
|
| self.future.poll().map_err( |x| JSError::new( x.description() ) ).and_then( future::result )
| ^^^^^^^^
| |
| expected signature of `fn(futures::Async<std::result::Result<A, JSError>>) -> _`
| found signature of `fn(std::result::Result<_, _>) -> _` error[E0507]: cannot move out of borrowed content
|
| self.future.map_err( |x| JSError::new( x.description() ) ).and_then( future::result ).poll()
| ^^^^ cannot move out of borrowed content It seems there aren't any combinators that will work for this situation, so I decided to just use manual pattern matching: fn poll( &mut self ) -> Poll< Self::Item, Self::Error > {
match self.future.poll() {
Ok( Async::Ready( Ok( a ) ) ) => Ok( Async::Ready( a ) ),
Ok( Async::Ready( Err( e ) ) ) => Err( e ),
Ok( Async::NotReady ) => Ok( Async::NotReady ),
Err( e ) => Err( JSError::new( e.description() ) ),
}
} This should be much better and faster than using |
I added in an I did some light testing in the Thanks a lot @CryZe for your |
I added in a new This is a necessary convenience, because the alternative is to tell the user to handle the errors themself, and it's easy to get error-handling wrong (e.g. accidentally silently discarding errors, printing the errors in a bad format, etc.) However, I don't like the type of The reason it requires I've thought about possibly adding in a new trait which can be used to give special behavior to Any ideas or suggestions for how to handle this situation are appreciated. |
My current preferred solution is to add in this impl: impl From< stdweb::web::error::Error > for () {
#[inline]
fn from( error: stdweb::web::error::Error ) -> Self {
js! { @(no_return)
console.error( @{error} );
}
()
}
} And now it's possible to use PromiseFuture::spawn(
create_some_future()
.from_err()
); I don't like that it has side effects in the implementation of |
After thinking about it, I decided on this solution: I added in a impl Error {
#[inline]
pub fn print( &self ) {
js! { @(no_return)
console.error( @{self} );
}
}
} I also removed PromiseFuture::spawn(
create_some_future()
.map_err(|e| e.print())
); I think this is ready to merge now. @koute @CryZe @Diggsey requesting a final review on this. |
The huge todo comment in my Executor still needs to be fixed. Basically a Future could resubmit itself while it is currently executing, at which point the RefCell will deadlock (panic), which is probably not what we want. The solution would be to try_lock the RefCell and on unsuccessful locking, queuing up the future for automatic re-execution when the current execution is done. That should most likely either be a |
@CryZe How would a Future resubmit itself? Ownership is passed into |
The Future gets the Task Notifier Handle during its execution, at which point at any time it can call that and will deadlock, if it's still executing. I've at least encountered one situation where this did indeed happen. Usually it's not a problem with other executors, as they just queue up the futures to be reexecuted, but here we just lock up, as with this executor, we try to immediately execute everything. |
@CryZe I see, I'll try and fix it. Though I don't fully understand the internals of the Future/Executor system, so it would be very helpful if you have a small test case to reproduce it! |
If you can give me edit rights on this PR I can fix it real quick if you want. (Alternatively I can do a PR on the PR branch) |
@CryZe I made you a collaborator on Pauan/stdweb. The code is in the |
Alright, I think I did it. You may want to run some more testing on this, as there was a lot more subtleties involved than I thought. |
@CryZe Why is it using Also, what are you using to test that it's correct? |
This needs to be behind a Cell / Lock / Atomic type in order to allow sharing + mutating it. Everything else is undefined behavior. (The pointer types actually may turn off those compiler optimizations already, but I'd rather not introduce some hidden undefined behavior somewhere. And this indicates that synchronization is necessary if WebAssembly ever gets proper threading. So I didn't do much testing yet. I'll probably test it a bit more tomorrow. |
@CryZe Ahhh, I see, having multiple The changes look good! I'll also see if I can get some tests going tomorrow. |
src/webcore/promise.rs
Outdated
// 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 | ||
pub fn promisify( input: Value ) -> Promise { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why not just keep the name resolve
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Because that name is super confusing.
If I see the name promisify
I have a pretty good idea of what it does (it probably converts something into a Promise).
If I see the name resolve
, I would never in a million years think that it converts something into a Promise. I would probably assume that it is somehow mutating an existing Promise or something (which isn't what it does).
Also, I have thought about changing it so that the promisify
function only works on fake Promises, not arbitrary values, so its implementation won't be as simple as just Promise.resolve(value)
I'm open for suggestions on changing its name, but resolve
is just a bad name for what it does. It's a bad name in JavaScript and an even worse name in Rust.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There is value in sticking to the original method names though, and I don't think promisify
is any more useful a name - you can figure that much out by the type signature.
As a compromise, how about Promise::resolved(..)
- this at least better indicates that it's constructing a new, resolved promise rather than modifying an existing one, while being very close to the original name.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I can compromise with Promise::resolved
, though I do worry about the confusion for people who are used to JavaScript (they need to remember that it ends in d
in Rust, but doesn't in JavaScript)
I think for that reason Promise::resolve
would be better than Promise::resolved
, though both are quite bad.
The more I think about it, the more I prefer the solution of changing Promise::promisify
to only convert fake Promises, and then the name makes more sense. Or perhaps rename it to Promise::convert
, I'm still open to name suggestions.
@CryZe I don't think the executor is correct. When I tried testing the code, I was able to get it to dead-lock, because the executor loop never yields to the JavaScript event loop. I think we need to insert a |
I think I fixed the deadlock, but I'm not sure if it's memory safe. I added in a |
It seems it isn't memory-safe: I get an "already borrowed: BorrowMutError" error, which I don't think ever happened with the old code. |
I think the reason it was causing problems was because I think I fixed it by adding in an |
…alls to task.notify()
Well, if there aren't any other objections, then @koute could you please review and merge this? The remaining issues (adding in web APIs like |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for all of the great work guys!
/// | ||
/// [(JavaScript docs)](https://developer.mozilla.org/en-US/docs/Web/API/Console/error) | ||
#[inline] | ||
pub fn print( &self ) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could we remove this method?
I understand the need to have convenience methods, but I'd prefer if we'd just wrap console.error
itself so that you could do console::error(e)
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
That's fair, though I had kind of figured that console.error(e)
was a placeholder, and we might implement better error handling in the future, in which case having e.print()
would be nice. But I don't have a particularly strong opinion about it, so I don't mind removing the print
method.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Since we don't currently have a console::error
function, I'd rather leave this in for now. Afterwards I'll make another pull request which adds in console::error
and removes print
src/webcore/promise.rs
Outdated
// 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 | ||
pub fn convert( input: Value ) -> Option< Self > { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think accepting a &Reference
here would be better than a Value
. (This will always return None
for anything which isn't a Reference
anyway, so I think it's better to statically signal that in the type signature instead of pretending that it can accept any Value
.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, perhaps something like from_nonstandard_promise
would be a better as a name? (Since it's more descriptive that way and as you've wrote in the comment yourself - it won't be used very often anyway.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
But if it accepts Reference
then you would need to do something like this:
Promise::convert(js!( return ... ).try_into().unwrap())
...which is annoying, verbose, and slower. And it doesn't really help, because even with a Reference
it still might return None
, so you don't gain any real static type safety.
Also, I've noticed that sometimes objects get turned into Object
(not Reference
), and the Promise::convert
function works with any object which happens to have a then
method (even Arrays!). So I think using Value
is correct.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Also, I've noticed that sometimes objects get turned into Object (not Reference)
This is actually a very good point, thank you! Hmm...
The original idea was that Object
objects represent a plain, dumb, JavaScript object for holding data, but unfortunately a lot of "fat" objects (e.g. like Bluebird promises) in the wild seem to be indistinguishable from those which are just simple {key: value}
containers for data. I'm thinking that perhaps we should stop treating Object
s and Array
s in a special way, make them a normal #[derive(ReferenceType)]
types and only have a Reference
variant in Value
.
I'm starting to really like this idea; then the problem of promises (and other kinds of objects) being converted to an Object
will disappear.
And it doesn't really help, because even with a Reference it still might return None, so you don't gain any real static type safety.
(Assuming we'll remove Value::Object
variant.)
- Yes, but, conversely if someone already has a
Reference
they have to convert it to aValue
now to pass into this function. - The types are there not only for guaranteeing type safety, but also to serve as documentation.
- Casting a
Value
to aReference
is relatively cheap as it's just amatch
. - You could also convert it this way:
js!( ... ).as_reference().and_then( Promise::convert ).unwrap()
, which I don't think is that bad?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we remove the Object
and Array
variants, then yes I agree it would make more sense to make it Reference
. But it's still a lot more verbose than using Value
, so I'm not sure.
I'll leave it as Value
for now, with a TODO note to change it later (if we decide to remove Object
and Array
variants).
src/webcore/promise.rs
Outdated
/// ``` | ||
// 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 | ||
pub fn to_future< A >( &self ) -> PromiseFuture< A > |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be cfg
-gated with the futures
feature; ditto for other futures
-related code.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What about the pub use webcore::promise::{Promise, PromiseFuture};
code in lib.rs
? Should that also be cfg
-gated?
Also, is it enough to only cfg
-gate PromiseFuture
, and not the to_future
method?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Both PromiseFuture
and to_future
need to be feature gated. Promise
itself should not. (You can split the pub use
line in lib.rs
into two lines.)
src/webcore/promise.rs
Outdated
pub fn done< A, B >( &self, callback: B ) | ||
where A: TryFrom< Value >, | ||
A::Error: std::error::Error, | ||
B: FnOnce( Result< A, Error > ) + 'static { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The error handling needs some work here.
- I don't think using
Error
(as in - the JavaScript typeError
) here is appropriate, as technically you can throw things which are notError
objects in JavaScript. It should be possible for the user to be able to retrieve the original value from promise's rejection. - It would be nice to figure out how to fit the
ConversionError
in this somehow. Perhaps convert it to a JavaScriptTypeError
? (This issue is also relevant in many other places acrossstdweb
when we currently panic, but where I'd like to eventually throw a JavaScript exception when a type conversion fails.)
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, good catch, you're right that technically any type can be an error, so it should be Result<Value, Value>
(which can be auto-coerced into something more precise by the user if they desire).
It would be nice to figure out how to fit the ConversionError in this somehow.
What do you mean? It doesn't panic, it converts the ConversionError
into a JavaScript Error
. So what are you suggesting? To convert it into a JavaScript TypeError
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What do you mean? It doesn't panic, it converts the ConversionError into a JavaScript Error. So what are you suggesting? To convert it into a JavaScript TypeError instead?
Yes, that's what I'm suggesting, since essentially a ConversionError
is our counterpart of JS' TypeError
. Another way to handle this would be to have an enum with two variants: one would be a Value
, and another would be... something else (perhaps even the ConversionError
if we make its innards private) which would signify a type error but without converting it to a JS type (keeping it purely in the Rust land).
I'm not sure which one would be better, but I'm probably leaning towards the first one's direction.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I tried changing it to allow for arbitrary types for the error, but I got a lot of type errors (that I wasn't able to fix), so for now I'm just going to .unwrap()
the ConversionError
errors, but I'll add a TODO note to fix it later.
@koute I changed Which also means it's now possible to be more precise with the error type (e.g. I also renamed I also fixed the Things to do after this pull request is merged:
I'll do as many of these as I can, but anybody else can do them if they want to. |
Ok, I'll merge it in as-is, but as discussed this still needs some work so if by the time I'll be releasing Thanks for the great work! |
@Pauan FYI, I've just got rid of the |
Fixes #72
@koute @CryZe This is untested and is definitely not ready to merge.
But I wanted to at least get started on adding in Promise support to stdweb. This pull request adds in the ability to convert any JS Promise into a Rustified Futurified version of that Promise. This conversion happens automatically with the
TryFrom<Value>
trait.Please look this over and let me know if you see anything weird, or anything that can be improved.