petkaantonov / bluebird

:bird: :zap: Bluebird is a full featured promise library with unmatched performance.
http://bluebirdjs.com
MIT License
20.45k stars 2.34k forks source link

3.0 cancellation overhaul #415

Closed petkaantonov closed 8 years ago

petkaantonov commented 9 years ago

The current cancellation seems to be the most problematic feature due to some annoying limits in the design:

Since all consumers and producers must be trusted/"your code", there is no reason to enforce that cancellable promises are single-consumer or create new primitives.

Edit: The below design has been scratched, see https://github.com/petkaantonov/bluebird/issues/415#issuecomment-73242358

In the new cancellation you would register the callback while marking the promise cancellable:

promise.cancellable(function onCancel(reason) {

});

This returns a new cancellable promise (unlike right now which just mutates existing promise which causes a lot of headache internally) and the flag will automatically propagate to all derived promises. However, the reference to the onCancel callback will only be held by the new promise created by .cancellable(). Calling .cancel() on a cancellable promise will keep propagating to cancellable parents and calling their onCancel callbacks.

The onCancel callback will receive the cancellation reason as its only argument (the default is a CancellationError instance).

From the callback it's possible to decide the fate of the promise:

(However I guess 99.99% of the time you want throw reason, so this flexibility adds a lot of inconvenience?)

Bad (should use .timeout() for this) example of usage:

function delay(value, time) {
    var timerId;
    return new Promise(function(resolve, reject) {
        timerId = setTimeout(function() {
            resolve(value);
        }, time || value);
    }).cancellable(function(reason) {
        clearTimeout(timerId);
        throw reason;
    });
}

var after500ms = delay(500).catch(CancellationError, function(e) {
    console.log("Your request took too long");
});
delay(250).then(function() {
    after500ms.cancel();
});

benjamingr commented 9 years ago

Summoning some relevant people - @kriskowal @domenic @spion

benjamingr commented 9 years ago

I agree that current cancellation is bulky and strange so a big +1 on overhauling it - it's a constant source of confusion for people in SO and it's not very intuitive IMO.

@kriskowal raised the argument that cancellation is inherently impossible. Here is the original discussion

I tend to agree that the major source of confusion is the dependency graph.

The most basic confusing case is:

                   A(cancellable promise)
      /-----------|------------\
      |                        | // children
      B                        C 

The way propagation works in this scenario is not obvious when you cancel B - in the current design suggested it will propagate to C (it has to) which might be unexpected to users


I'm asking why (like progression) not do this at the user level? Why do we need a special construct?

function delay(value, time, token) {
    var timerId;
    return new Promise(function(resolve, reject) {
        timerId = setTimeout(function() {
            resolve(value);
        }, time || value);
            token.cancel = function(){
                 clearTimeout(timerId);
                 reject(new CancellationError());
            };
    });
}
var token = {cancel: function(){}};
var after500ms = delay(500, token).catch(CancellationError, function(e) {
    console.log("Your request took too long");
});
delay(250).then(function() {
    token.cancel();
});

This way anyone holding the token can cancel so who has the ability to cancel is explicit.

It seems like most times promises require cancellation of the direct problematic promise (the HTTP request, the timeout etc) - if we're not creating new primitives with single-consumer can't we give up all of propagation?

This is what languages like C# or Scala do (for example: http://msdn.microsoft.com/en-us/library/dd997396%28v=vs.110%29.aspx ) although I have to admit I'm not a fan of what C# does to be fair.

MadaraUchiha commented 9 years ago

I tend to agree with Benjamin (when do I not?), this seems like an awfully lot of complexity to add to something that can be relatively simply implemented in userland.

petkaantonov commented 9 years ago

There has been many issues posted about cancellation, literally none about multiple consumer afaik. Anyone can call _reject on any promise so there is no security issue which I understood the whole impossibility thing was based on.

Progress is simply a callback that doesn't even need error handling (and if it does there was no good way to do it even when integrated in promises, what bluebird does when progress handler throws is absolutely horrible).

However there are multiple issues with the token:

UndeadBaneGitHub commented 9 years ago

Excuse me for bumping in, but I would support Petka's implementation with the onCancel callback. As he has rightly observed, the absolute majority of complains (tbh, mine as well) lies in the field of using cancellable with collections. Say, with Promise.map we have to do use something like this:

var collectionItemsWrappingPromises = [];
var someCollection = [1,2,3];
return Promise.map(someCollection, function(collectionElement) {
    collectionItemsWrappingPromises.push(new Promise(function(resolve, reject) {
         console.log(collectionElement);
         //now do something lengthy
    })
    .cancellable());
    return collectionItemsWrappingPromises[collectionItemsWrappingPromises.length - 1];
})
.catch(Promise.CancellationError, function(err) {
           collectionItemsWrappingPromises.forEach(function(collectionItemWrappingPromise) {
                collectionItemWrappingPromise.cancel();
            });
            throw err;
         });

This is, essentially, the very same token idea, implementing at the user level and it is hellishly inconvenient.

It is ok to do it once, maybe twice. But if the application is complex enough, one has to do it all the time, constantly minding the hierarchy of collections stored. Let alone creating some heavy-loaded apps, which require stuff like Mozilla's node-compute-cluster or similar. It becomes really messy really fast, so making it a bit more obscure syntax-wise (I mean that return/throw thing) but way more convenient, I'd say, would be a great plus.

petkaantonov commented 9 years ago

By issues I was mostly referring to github but I also checked stackoverflow now.

https://stackoverflow.com/questions/27479419/bluebird-promise-cancellation - No idea what they were trying to do here but seems to be related to the first issue of having to use .catch to attach cancellation hooks.. although they aren't even using that hook to cancel anything (Awkward API for attaching cancellation hook +1)

https://stackoverflow.com/questions/27132662/cancel-a-promise-from-the-then - Needed to use throw instead of cancel (Also trying to compose with .props which wouldn't work, collection composability +1)

https://stackoverflow.com/questions/24152596/bluebird-cancel-on-promise-join-doesnt-cancel-children - Join doesn't compose (collection composability +1)

https://stackoverflow.com/questions/23538239/catch-a-timeouterror-of-a-promise-beforehand - (Awkward API for attaching cancellation hook +1)

So that's only 4 out of 203 related to cancellation, and all are at least related to the top 3 issues posted in the OP. I only checked the bluebird tag.

benjamingr commented 9 years ago

@petkaantonov :

There has been many issues posted about cancellation, literally none about multiple consumer afaik. Anyone can call _reject on any promise so there is no security issue which I understood the whole impossibility thing was based on.

I've seen it on StackOverflow several times - it's not about encapsulation at all it's about propagation not being simple to reason about. What happens when you Promise.all from a cancellable? What happens when it has multiple consumers and cancel one that also consumed (through a return in then) another cancellable? And so on.

As for tokens:

Doesn't compose (or at least I don't see how it could) with the collection methods which I believe is the most common issue

Yes! This is the point. I'm saying I think I don't want them to compose since if you want to cancel something you just need a reference to the original promise's token. This circumvents the hierarchy problem.

Makes user create their own CancellationError which has been standard since version 0.x

Why can't they throw Bluebird's Promise.CancellationError?

The inconvenience of having to create a weird pass-by-reference emulation caller side

Yes, this is pretty weird but then again it's also how other languages do it. The reason it is passed to the function (rather than let's say - monkey patched) is that it is conceptually not the concern of the returned promise but the concern of the action itself (a promise just represents an eventual value - the function producing it is the one concerned with cancellation and progression). I think putting cancellation on the promise itself is definitely complecting it.

Also here are more questions: https://stackoverflow.com/search?q=%5Bbluebird%5D+cancel+is%3Aquestion

@UndeadBaneGitHub :

Excuse me for bumping in

This thread is here for feedback and discussion - if people weren't bumping here there would be no point :)

How would you expect aggregation to work with cancellation in general? How would all work? How would any?

petkaantonov commented 9 years ago

I've seen it on StackOverflow several times - it's not about encapsulation at all it's about propagation not being simple to reason about. What happens when you Promise.all from a cancellable? What happens when it has multiple consumers and cancel one that also consumed (through a return in then) another cancellable? And so on.

You don't typically actually have multiple consumers for any promise when using .all (or any other collection method afaik). The .all PromiseArray is consuming the input promises while the user is consuming the output promise of .all. Cancelling the output promise of any collection method will call cancel on all the promises related to the collection.

UndeadBaneGitHub commented 9 years ago

@benjamingr Let's take the example you've posted. Right now the user has to hold on to a manually managed collection of C promises and just write B in a way when he is ok with it finishing and its result would be uncontrolled. And then, C might have D (cancellable), D might have E (also cancellable) and so on. Lots of collections, lots of catch(Promise.CancellationError), function(err)) and a ton of dirty code. I see what @petkaantonov has offered to be a nifty syntax sugar, saving time of the users. It does not solve problems with composition (I doubt that this can be solved in a convenient way at all), but looks far better, than the present implementation. any is an interesting quesiton, though.

benjamingr commented 9 years ago

@petkaantonov :

Cancelling the output promise of any collection method will call cancel on all the promises related to the collection.

So Promise.race([p1, p2, p3]).cancel() will cancel all promises in the race?

What about:

 var p = Promise.race([p1, p2, p3]);
 p.then(function(){
      p.cancel();
 });

This would cancel every promise in the race except for the one that resolved?

What about:

 var p5 = Promise.map([p1, p2, p3, p4], function(elem){
     p5.cancel();
});

Would it cancel all promises except for p1 except for those that did not resolve first?

What about:

 p1 = getCancellablePromise();
 var p2 = p1.then(doSomethingElse);
 var p3 = p1.then(doSomething).then(function(){ p1.cancel(); });

Do you think users will understand this is a race condition between p2 and p3?

I'm just not convinced this is easy to reason about for the general collection method case - kind of like progression. Cancelling an operation itself seems orthogonal to the returned promise for the value.

Also - how would you cancel mid-chain? The whole .cancellable syntax seems kind of quirky to be fair. I haven't formed a strong opinion on the new syntax yet but I'm wondering if we need a bigger overhaul here.

@UndeadBaneGitHub :

Right now the user has to handle aggregation of cancellations manually - my argument here is that there is no generic way to always aggregate cancellation in a way the user would find predictable. With the token approach you'd have something like:

 var tokens = [];
 var myRequests = Promise.map(urls, function(url){
       var token = {cancel: function(){}};
       tokens.push(token);
       return makeRequest(url, token);
 });

Now, we can cancel all the requests with tokens.map(function(el){ return el.cancel(); }, or cancel a specific request with tokens[4].cancel() or we can aggregate them by wrapping it in a function:

 function getRequests(urls, token){
   var tokens = [];
   var myRequests = Promise.map(urls, function(url){
       var token = {cancel: function(){}};
       tokens.push(token);
       return makeRequest(url, token);
   });
   token.cancel = function(){ tokens.forEach(function(t){ t.cancel(); };
   return p;
 }

However since we did this little wiring ourself we can do all this wiring to match what we actually want - cancel all of them, or just one, or maybe the last two - it's in our side and it's not as ugly as in your example at all :)

petkaantonov commented 9 years ago

@UndeadBaneGitHub What problem with composition is not solved?

Cancelling a promise which is a promise for the result of any collection method will cancel the input promise-for-array if it's still pending.

Otherwise cancelling a promise which is a promise for the result of .all, .some, .any, .props, .settle or .race will cancel all the input promises which are still pending.

Otherwise cancelling a promise which is a promise for the result of .each or .reduce will cancel the currently awaited-on promise.

Otherweise cancelling a promise which is a promise for the result of .map or .filter will cancel all the currently in-flight promises, original input or mapped ones.

petkaantonov commented 9 years ago
 var p = Promise.race([p1, p2, p3]);
 p.then(function(){
      p.cancel();
 });

That's a no-op, since p is not cancellable (cancellability means not pending and having a cancellation flag) at that point.


 var p5 = Promise.map([p1, p2, p3, p4], function(elem){
     p5.cancel();
});

Cancel will be no-op after the first call, but most likely the cancellation hook will reject an input promise which causes the whole map to reject and no other elements will be then iterated.


 p1 = getCancellablePromise();
 var p2 = p1.then(doSomethingElse);
 var p3 = p1.then(doSomething).then(function(){ p1.cancel(); });

No-op since p1 must be resolved at the point where you are calling p1.cancel()

benjamingr commented 9 years ago

That's a no-op, since p is not cancellable (cancellability means not pending and having a cancellation flag) at that point.

That makes sense although it's very common to want to cancel all other requests once one has resolved in races or somes.

causes the whole map to reject and no other elements will be then iterated.

That's kind of scary given that iteration might have side effects in peoples' code.

No-op since p1 must be resolved at the point where you are calling p1.cancel()

Still if you call p2.cancel there is a race condition - but that's inherent to what cancellations means though so that's irrelevant.

petkaantonov commented 9 years ago

That's kind of scary given that iteration might have side effects in peoples' code.

That's not related to cancellation though since any promise can also reject naturally

petkaantonov commented 9 years ago

it's very common to want to cancel all other requests once one has resolved in races

Which is ironic because it's very rare to need .race. I agree about .any and .some though - in that case there is the inconvenience of having to .cancel the input array manually after .any has succeeded. However, those methods can take option autoCancel if it's a real problem.

UndeadBaneGitHub commented 9 years ago

causes the whole map to reject and no other elements will be then iterated

Now this makes me kinda concerned, as it might look like this:

 var p5 = Promise.map([p1, p2, p3, p4], function(elem){
    // something really lengthy here
     p5.cancel();
});

This looks to me like a race condition with completely unpredictable behavior. What if p1, p2, p3 or p4 is a non-cancellable, but already iterated, as the first then took long enough?

petkaantonov commented 9 years ago

Replace p5.cancel() with anything that throws and you have exactly the same problem, no?

UndeadBaneGitHub commented 9 years ago

Fair enough, but I guess, this is a matter of what you want cancel to be and how it should be perceived. If it is like "this is is just some sugar, but in the end the behavior is as unpredictable and (potentially) destructive to your app state as throw is" - then it is fine as it is. Personally, I think that is still a great improvement over the existing mechanism, and I am ok with that as a user - just always keep in mind, that cancel might kill your state. I mean, throw in a properly constructed code usually means "ok, this block is dead, let's clean it up and crash/start anew". At the moment, cancel resides at the very same place, and in the overhaul you've offered it stays there, but the is just "sugarized", which can lead to some confusion. But if you want to go for the approach "cancel is a separate and predictable mechanism and not just a sugar for a specific exception type" I guess some greater overhaul is required, not sure which for the moment. Hell, I'm not even sure it is possible with async architecture.

petkaantonov commented 9 years ago

Well it doesn't have to be throw, just having the promise reject that you return from .map has same effect. Note that cancellation doesn't have to throw at all, you can for instance make the cancelled promise fulfill with null when it's cancelled.

spion commented 9 years ago

By its very nature .cancel() is a source of race conditions. If you think about it, the essence of the feature is to give you the power to interrupt one or more ongoing operations mid-flight from another chain of execution. The sync equivalent of cancellation is killing a thread (ok, its slightly better, but its pretty close once many promises are involved). There is just no way to make it safe and predictable.

We don't use the feature in our code, at all. In most of the cases we've encountered using it is just premature optimization. In other cases, the problem is better solved with streams. In the very few cases where its really necessary, we split up the operation into multiple atomic actions and race them with a rejectable (unresolved) promise which serves as a way to interrupt the execution.

benjamingr commented 9 years ago

I tend to agree with @spion - in general we use cancel in very few places in our code.

UndeadBaneGitHub commented 9 years ago

Well, I use cancel not as something like Thread.Abort or TerminateThread, rather as a "stop processing" message in the infinite message loop. Thus, all the promises are cancellable, and cancellation handlers properly handle the resources if called, but. It took quite a bit of effort to construct such a thing, so I would really prefer cancellation going the following way: Say, we have that very example:

var p5 = Promise.map([p1, p2, p3, p4], function(elem){
    // something really lengthy here
     p5.cancel();
});

And p1, p3 and p4 are cancellable, but p2 is not. Thus, if all promises got iterated and then p5.cancel(); happens, p1, p3 and p4 should have their onCancel callback called, while p2, being non-cancellable, just run till the end of it.

This sounds to me like a more or less predictable behavior (yes, still a race condition and you don't know whether p2 was called at all), but resources-wise you can guarantee, that if p2 starts, nothing wonky would happen with the resources it uses, and p1, p3 and p4 must be implemented in a specific, cancellable way, or make the app unstable. And the onCancel mechanics would separate this behavior from throw and reject ones.

spion commented 9 years ago

@UndeadBaneGitHub thats a rather interesting interaction between .map and cancellation. Do you have a slightly larger example that we could look at (with an infinite message loop)?

I know that .map is implemented differently (not just sugar) now, but if the code was written instead with Promise.all([p1,p2,p3,p4]).then(list => list.map(f)).all(), would you still expect f to execute on p2?

UndeadBaneGitHub commented 9 years ago

@spion I would really appreciate, if you wrote the code in a bit more detail. If you mean this:

var p5 = Promise.all([p1, p2, p3, p4], function(list){
    list.map(f);
});
p5.cancel();

then list.map(f) would just not happen, if cancellation comes while all p1, p2, p3 and p4 have not yet been completed. However, p2, if already iterated, must be allowed to finish in my variant of how cancel works.

spion commented 9 years ago

Its

var p5 = Promise.all([p1,p2,p3,p4]).then(function(list) { 
  return list.map(f); 
}).all();
lengthy.then(function() { p5.cancel(); });
benjamingr commented 9 years ago

Every C# person I talked to is against conflating cancellation with promises directly. I do think that the library should help users with that but perhaps it is best if it is not a part of the promise itself.

F# passes the token implicitly - I'll look into it.

rogierschouten commented 9 years ago

We've had a discussion about this internally, and we came up with the following:

  1. The suggestion by @petkaantonov to have the cancellation callback instead of the catch-thing is an improvement, especially since we tend to use catch for programming errors and not for run-time control flow (and we tell our linting tool to disallow Promise.catch())
  2. Propagation/composition is a different issue altogether and so far @petkaantonov's composition overview seems reasonable
  3. Our use cases fall into two categories: either very simple (propagation/composition not necessary) or too complicated to be able to use the built-in bluebird cancellation anyway - typically we need a 'nice' cancel (finish a piece of work and stop) and an abort-type cancel (e.g. destroy underlying socket so that the work fails).

So in short, we support both @benjamingr and @petkaantonov 's suggestions in that we like the proposal for the new cancellation, and simultaneously, that real-world problems often need something custom.

benjamingr commented 9 years ago

@rogierschouten thanks a lot for taking the time to do this and for the feedback!

So in your use cases propagation was not necessary? Just making sure we're on the same page:

 cancellableA().then(foo).then(bar).then(baz).cancel(); // this is propagation
rogierschouten commented 9 years ago

@benjamingr You're right I meant composition. Propagation is necessary.

petkaantonov commented 9 years ago

Ok so I have changed my vision for cancellation completely. It will be mostly motivated by client side use cases: e.g in an SPA you want to cancel all requests related to a page when user changes page, closes a widget and so on. Another example is ajax autocomplete.

There will be no state changes to any promise when cancelled: to user of a promise it simply appears infinitely pending - the only difference from an actually infinitely pending promise is that memory is not leaked. There is no reason for the upstream to have any special reaction to this form of cancellation at all, and the downstream now only simply cancels/disposes the work, nothing more.

Hello world example:

function cancellableDelay(ms) {
    var handle;
    return new Promise(function(resolve) {
        handle = setTimeout(resolve, ms);
    }).cancellable(function onCancel() {
        clearTimeout(handle);
    });
}

var p = cancellableDelay(500)
    .then(function() {
        alert("hello world");
    });

Promise.delay(250).then(function() {
    // p and its descendants now appear infinitely pending but no memory is leaked
    // unlike in the situation where user had called `clearTimeout` out-of-band
    p.cancel();
});

Like a disposer, cancellable callback should not throw, it should simply clean up resources. If it throws, the reaction is same as in a disposer, treated as a fatal system error not a promise rejection.

Collection composition

Like before, this supports var a = Promise.collectionMethod(); a.cancel() calling automatically cancel() on all related promises.

However, a concept of weak and strong cancellation is introduced to support the desirability of e.g. Promise.all implicitly cancelling all promises when one rejects or Promise.any implicitly cancelling all still pending promises when one succeeds.

If cancellable promises want for some reason to opt out of weak cancellation, it returns false from the handler:

.cancellable(function (cancellationType) {
    if (cancellationType === "weak") return false;
});

So a weak cancellation originates from some implicit mechanism while a strong cancellation always originated from an explicit .cancel() call.

What about CancellationError?

CancellationError will be completely unrelated to the new cancellation, however the error type is retained (it must be retained for compatibility) which can be used as a custom rejection reason in situation where it suits. The new cancellation is simply about not caring about some result and freeing all resources and memory related to it, nothing more. No third state.

What about the theoretical incorrectness of associating cancellation with promises?

For the kind of cancellation described above I cannot see any problem with it, separating it from the promise object only servers as a practical annoyance of having to pass around 2 values - if you are gonna pass them together 100% (or nearly 100%) of the time, they might as well be in the same object.

petkaantonov commented 9 years ago

Btw what sparked the idea was @WebReflection 's post at https://esdiscuss.org/topic/promise-returning-delay-function#content-10 , I realized it's exactly what I wanted from cancellation as well.

benjamingr commented 9 years ago

I'm going to need a few days to evaluate and formulate an opinion on this.

spion commented 9 years ago
using(openResource(), res => {
  return someCancellable().then(x => res.use(x));
});

If the promise resulting from someCancellable() gets cancelled, the resource will leak.

petkaantonov commented 9 years ago

@spion using can add its own onCancel callbacks to do clean up if the promises passed to it or returned to it are cancellable.

spion commented 9 years ago

I'm just saying its a potentially huge breaking change with implications far and wide. e.g. anydb-sql transactions will break if someone cancels a promise that happens during a transaction, unless updated.

petkaantonov commented 9 years ago

You can't have any cancellable promises unless they are aware of the new API.

I.E. if you have in your code right now .cancellable(), that's not going to work because the new cancellable takes a function and returns a new promise.

spion commented 9 years ago

yes but what if someone uses a new, cancellable promise (that has nothing to do with the db) within my transaction block?

petkaantonov commented 9 years ago

Well you'd have to modify your transaction block code to react to cancellation if you need to support 3.0

spion commented 9 years ago

Which is why I said, a potentially huge breaking change :)

petkaantonov commented 9 years ago

@spion FWIW, generator cancellation which seems to work in the same way, does this:

The close method terminates a suspended generator. This informs the generator to resume roughly as if via return, running any active finally blocks first before completing.

http://wiki.ecmascript.org/doku.php?id=harmony:generators#internal_methodclose

spion commented 9 years ago

Interesting. Doesn't make it any less weird, but at least there is consistency.

Since I don't really use cancellation, I withdraw from the debate. I still have my doubts about the solution, but if people who use cancellation like it...

UndeadBaneGitHub commented 9 years ago

@spion We so do! Current cancellation implementation requires really enormous amount of additional code to be written and maintained to get the desired (described by @petkaantonov, more or less) behavior.

@petkaantonov the question remains, however, would the cancelled "infinitely pending" promises, if cancellable callback was not defined, be forcefully stopped and memory freed, or they will just run and call resolve/reject into the void?

petkaantonov commented 9 years ago

@UndeadBaneGitHub not sure what you mean, only finally handlers will be called (since if any resource clean up is necessary it is in finally handlers) and nothing else.

Artazor commented 9 years ago

@petkaantonov I'd support "drop-the-chain" semantics, just want to clarify:

cancellablePromise.then(function(){
     console.log("A:resolved");
}, function(e) {
     console.log("A:rejected");
     throw e;
}).finally(function(){
     console.log("A:finally");
}).then(function(){
     console.log("B:resolved");
},function(e){
     console.log("B:rejected");
     throw e;
}).finally(function(){
     console.log("B:finally");
}).cancel();

Will produce only

A:finally
B:finally

Won't it? So finally becomes more important construct than it was before.

petkaantonov commented 9 years ago

@Artazor yes but it doesn't make it any more important than it is currently - if you need to restore state or clean up resources then you would use finally/using anyway.

Artazor commented 9 years ago

@petkaantonov of course! I mean that it now can't be emulated by .then(always,always) (rougly). I know the difference (non affecting the result)

Artazor commented 9 years ago

@petkaantonov Seems that full implementation of this pattern is in contradiction of the current Promise/A+ spec, since it will involve a new state of the promise: "cancelled" (and cancellable promises should be a part of the specs as well as .finally() and .cancel()).

Meanwhile I don't feel that the theoretical implications of the proposed pattern are sound. Cancellation as described above naturally resembles the uncatchable exception that unwinds all stack frames of the awaiting chain (though running all the pending finally blocks and using disposers) and should be silently swallowed at the top level.

I feel that it is enough to achieve a sound "forgeting" semantics only when we deal with a single promise chain. However what the result is expected to be obtained from the following code:


// p1, p2, p3 - are cancellable
Promise.all([p1,p2,p3], function(list) {
     console.log(list.length); // I'd expect 2 here
});
p2.cancel();

or even


Promise.all([p1,p2,p3]).spread(function(a,b,c){
     console.log(a);
     console.log(b);
     console.log(c);
});
p2.cancel(); // I'd expect the whole spread cancellation here

?

Does it mean that there are more types of cancellation then the "weak" and the "strong" one? Or should the .spread() change it's semantics - should it be coupled with the list of what to be spread across the arguments of the handler?

And what about the .settle()?

petkaantonov commented 9 years ago

Promises/A+ only covers .then() and there is no new observable state or behavior for users of then. First you'd have to call .cancel() to begin with (weak cancellation was dropped) but also if you are only using .then you cannot tell the difference between infinitely pending promise and cancelled one.

.all only fulfills when all the promises fulfill, so you should always expect 3 as length. Given how the all promise rejects when one promise rejects in the array, the all promise will cancel if one promise in the array cancels.

.settle() has been deprecated in favor of .all(vals.map(val => val.reflect())) due to how confusing it is.

Artazor commented 9 years ago

Have you considered the case when we have a set of homogeneous pending promises and want to wait them all but cancelled, when the order of their resolution is non-deterministic (don't-care as opposed to the .all() guarantees).

Should we implement that behavior in a userland or can this be provided out of the box via some method like .collect([p1,p2,p3]).

Would it be useful to have an operation .maybe() that resolves into Maybe<T> even if the original promise is cancelled (and rejects if the original promise rejects)? Will it lead to any terrible anti-patterns?

petkaantonov commented 9 years ago

This kind of cancellation is only about forgetting and not caring while not causing leaks or inconsistent state. I think you want a race between rejection and fulfillment, combined with reflect/settle - not the don't-care-cancellation semantics