-
Notifications
You must be signed in to change notification settings - Fork 90
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
Observable should be a thenable #15
Comments
I considered this, but was concerned that someone might want to actually deliver an observable asynchronously through a promise. In other words, let promise = doSomethingAsync().then(_=> {
return new Observable(sink => {
sink.return(1);
});
});
promise.then(x => console.log(x)); // "1", not an observable I guess the question is, do we want to disallow |
@jhusain what is the fulfillment value of the |
The value returned to the Generator's return method. JH
|
sounds reasonable |
Yes, when I suggested this in the composition functions thread we also considered a special symbol instead of making it a
I can definitely think of situations where you'd want a |
Thinking out loud: what if the subscription were either a "thenable" or a promise subclass? If we allowed an empty argument list to "subscribe", we'd have: async function af() {
let observable = getObservableFromSomewhere();
let answer = await observable.subscribe();
console.log(answer);
} Thenable assimilation is kind of weird, so the suggestion would be making subscriptions a subclass of promise: class Subscription extends Promise {
unsubscribe() : undefined;
} Essentially a cancellable promise, with well-defined and reasonable semantics. : ) |
If that suggestion works out, then it could potentially remove the need for a separate "forEach" and the need for me to think of a response to #20 : ) |
We'll hopefully be having full-on cancellable promises sooner rather than later (driven by fetch, so, I'm talking in terms of low-single-digit browser-releases). It might be reasonable to block observable unsubscription semantics until they ship. |
@domenic are the conversations surrounding cancellation happening somewhere? It would probably be a good idea for me to at least listen. |
@zenparsing still booting up, maybe watch https://github.com/domenic/cancelable-promise but it's pretty empty right now. |
@zenparsing the public conversation about cancellable promises got really noisy really fast. If you want to see an example of similar semantics you can see the bluebird 3.0 cancellation semantics - they're not the same but they're close: http://bluebirdjs.com/docs/api/cancellation.html If you're used to Rx semantics this is similar to Subscription as a thenable or a promise subclass sounds very reasonable - doesn't really matter which but I definitely see the merit in a subclass since it gets all the other useful methods as well as ones added to the API in the future. |
I think cancel and unsubscribe are different (although maybe overlapping) To me:
For example, given: Promise.all([
fetch('/user/1'),
fetch('/user/2'),
Promise.reject(new Error('...'))
]) We could augment I believe what this means, is |
This is going to turn into another promise cancellation thread, I can tell :( |
Agree with Stefan. Although I agree it would be a nice unification, Cancellation and unsubscription are semantically different from one another. They provide different guarantees. JH
|
@jhusain What do you think about the general idea of making the subscription object thenable or a promise subclass, rather than the observable? |
Not a chance.
For what it's worth, some promise libraries have a notion of disposers (http://bluebirdjs.com/docs/api/resource-management.html). Observable dispose is in-between promise cancellation and promise dispose, with the big difference that an observable is lazy and disposing is done on the subscription itself.
I think it would be very nice to be able to await an observable itself without needing a subscription, this might be a weak argument but if we force everyone to It's also worth mentioning that if we're discussing async/await it might be nice to add semantics that allow hooking on a single value basically acting like async iterators (that is, a |
Agree with Benjamin that there is an ergonomic issue with awaiting an Observable by first calling subscribe. I hadn't considered the pain of this: await obs.subscribe(); I think users will expect to await Observables directly as they do in .NET. I also think this expectation would be correct, because thening an Observable is a subscription. You're kicking off the side effects and waiting for the result. The tradeoff is that you cannot cancel as long as Promises remain uncancellable. However a future proposal manages to fix that then we'll have the best of all worlds. JH
|
Do we normally think of "await" as kicking off side effects? |
No, |
I think "kicking off the side effects" is probably better worded as "kick off the operation" in case of a lazy construct. As Domenic said when awaiting eager constructs (like promises) the |
In essence both in I'm not sure I understand what's the problem with |
I guess it seems like both to me. A type conversion (what happens when you do |
Is it going to be clear that it seems like adding this so observable can be used with I think I'm with @domenic here in that you should have to call a method that returns a thennable object. Perhaps the subscription (generator) object that is returned from the
As I stated above, you could already use |
I guess the shorter version of what I'm saying is: Encouraging use of Observable in any way that doesn't allow you to cancel it seems like encouraging bad practice. I can totally be convinced otherwise, I guess. |
That's an interesting point @Blesh about cancellation. There is a great deal of things you don't want to or need to cancel in server-side code, typically DB requests for example, or web requests (on the server, not the client) and so on. In fact I've found that in the vast majority of the times I need cancellation I need concurrency anyway since the reason I'm cancelling is because what I need became irrelevant - in which case (All that said, it's totally possible to do cancellation with await using I also tend to agree with Domenic, we need to think of a way to make the syntax less confusing to people and still short - an I think in the meantime awaiting subscriptions which become thenable or subclasses of promise is the best we can do right now. This also unifies forEach and subscribe. |
I think they are likely to come to ES exactly as fast as observable is likely to come to ES. |
just had a thought, what about: try {
let something = await somethingAwaitable;
} catch (e) {
// cancel/unsubscribe somethingAvaitable ?
} and:
try {
var subscription = myObservable.subscribe();
var returnValue = await subscription;
} catch (e) {
// would e === 'cancel with an error' ?
}
subscription.error('cancel with an error'); |
try {
let something = await somethingAwaitable;
} catch (e) {
// cancel/unsubscribe somethingAvaitable ?
} I'm going to need a more concrete example to say how I would address it. |
|
@Blesh I think we need a real example. This would help the conversation. I believe the concern is the current work of promises, no back-propagation of un-interest exists. So as soon as you insert a promise at a "joint", you lose the ability to declare the lack of interest. Thereby losing the fidelity we have raw observables. That is, without introducing some side-channel. |
@stefanpenner A real example would be where you have an AJAX request wrapped in an observable, you kick if off when you're loading your view in a client-side app, then the user navigates to another view before the observable returns, and you want to cancel the observable. In that case, using Another would be the typical "autocomplete" text box, where you're getting some search results based on the last value entered, if the user hits a new key in the text box before the previous query returns, you want to cancel the previous query and await the next one. |
In concurrent cases and combinations As for ajax wrapped request - if the |
@benjamingr @stefanpenner to clarify, I'm making the case that: await myObservable; ...is not cancellable, because I've gotten no reference to anything I can call Therefor, I think it is more desirable that whatever is returned from |
I agree cancelable promises are an important building block here.
Yes, currently this proposal contains three methods all of which basically do the same thing (subscribe, [Symbol.observer], and forEach). In general any argument that applies to one of them applies to all. Although there is probably some room for trimming the fat. |
@domenic but is it a cancellable promise or a then-able subscription that is necessary here? I'd argue the latter, since having a |
Having a |
I think you should be able to
You're not the one that is cancelling it though, when the async function gets cancelled the inner |
@domenic ... ah, you're right it was never part of this proposal. I keep getting this confused with the original spec that this was modeled after, in which |
@Blesh We'll get that repo updated soon. |
It's worth mentioning that await has never guaranteed no side effects. Promise.resolve calls "then" on non-Promises and there's nothing about the thenable contract that guarantees no side-effects. I think developers think about await in terms of sequencing ("then" is pretty appropriate here). The definition of "then" as a subscribe on an Observable seems pretty intuitive to me. It's worth pointing out that in .NET you can await an Observable. Developers seem to be able to intuit the behavior. JH
|
Agreed.
Strongly disagree. The thenable contract is about type conversion between promise implementations, and fulfilling that contract means behaving like a promise---i.e. no side effects. It's true that Promises/A+ is phrased in terms of "you must do this" and not "you must do this and nothing else." But everyone would agree that "perform the usual Promises/A+ steps and then delete your C:\ drive" is not really fulfilling the thenable contract. Similarly, "perform an arbitrary action, then create something that performs the usual Promises/A+ steps" is not in the spirit of the contract either. |
@domenic (This is an honest question, I can't think if a better way to phrase it, sorry, don't read into it) How can a contract enforce no side-effects in JavaScript? |
it can't strictly enforce, rather by convention it can be enforced. |
Ah... I guess when I read "guarantee" I think "enforcement". |
Regardless of the original intent of the original spec, we should I think we should be focused on giving developers what they expect. We have On Mon, Jun 1, 2015 at 4:40 PM, Stefan Penner notifications@github.com
|
I've covered what the hazard is above:
I definitely agree. Which is why I think awaiting anything should not cause side effects, since that is what JavaScript developers will expect from using promises. |
JavaScript developers are using Promises, and presumably in this situation
they will also be using Observables. Is it not reasonable for them to
understand the semantics of the type they are using on the right-hand-side
of an await expression? If we assume no knowledge of Observable semantics,
the state hazard presents itself regardless of whether developers use await
or subscribe. If devs don't know Observables were stateful they could
inadvertently repeat side-effects by calling subscribe twice. Your argument
seems predicated on the idea that developers will only understand await in
terms of Promises, but if we make it work on Observables out-of-the-box,
why would that be so?
In a world in which await worked on both Promises and Observables, why
would developers assume await enforces Promise semantics? Why wouldn't they
assume it does strictly what it says: "await"? I'm concerned that it is
short-sighted to to limit the concept of "waiting" to Promises when we
might have another async type in the language.
Seems we are repeating our arguments, so let me propose a compromise that
might make everyone unhappy: returnValue(). This combinator would return a
Promise which would hopefully remove all ambiguities.
|
@domenic any objection to observable.returnValue()? Would like to have some solution for async/await integration before Babel inclusion. |
If it's |
@jhusain wouldn't returnValue() be equivalent to subscribe(), (with zero arguments) if the returned subscription were promise-y? |
@zenparsing that would require
|
@benjamingr it's not the case at the moment, but we could certainly allow that if we wanted to. |
Commented in other thread about hazard of making subscribe a promise. The reason to call it final value is that there is likely to be other combinators along the same lines: firstValue(), lastValue(), and so on. |
My objection to .returnValue() is that we are adding a fourth almost-does-the-same-thing method. I would much prefer the direction discussed in #20 (comment) to pare things down to one method instead of adding a fourth. |
@zenparsing could you clarify what was settled on here? How are subscriptions going to be awaited? |
@benjamingr agreed, I'm actually a little confused about this. Last I talked to @jhusain he had said |
This was an oversight. We should definitely spec this so that async await works on Observables out of the box.
the return value of the Observable will be come the resolution of the promise. Likewise errors become rejections.
The text was updated successfully, but these errors were encountered: