Aborting a fetch #27
Comments
This was referenced Mar 26, 2015
WebReflection
commented
Mar 26, 2015
|
Thanks for the effort folks, I'm chiming in to follow up and with a couple of questions:
Best Regards |
|
This is only somewhat-related to promises being cancelable. This is about cancelling a fetch. It does matter somewhat for one of the open issues and yes, we might end up having to wait or decide shipping is more important, we'll see. And we won't have a promise-subclass and a controller. Either will do. The subclass uses |
|
The controller approach is certainly the quickest way we'll solve this, but it's pretty ugly, I'd like to treat it as a last resort & try for the cancellable promises approach. Cancellation based on ref-countingI'm still a fan of the ref counting approach, and from the thread on es-discuss it seems that libraries take a similar approach. var rootFetchP = fetch(url).then(r => r.json());
var childFetchP1 = rootFetchP.then(data => fetch(data[0]));
var childFetchP2 = rootFetchP.then(data => fetch(data[1]));
var childP = Promise.resolve(rootFetchP).then(r => r.text());
childFetchP1.abort();
// …aborts fetch(data[0]), or waits until it hits that point in the chain, then aborts.
// fetch(url) continues
childFetchP2.abort();
// …aborts fetch(data[1]), or waits until it hits that point in the chain, then aborts.
// fetch(url) aborts also, if not already complete. Out of refs.
// childP hangs as a result
rootFetchP.then(data => console.log(data));
// …would hang because the fetch has aborted (unless it completed before abortion)Cancelling a promise that hadn't already settled would cancel all its child CancellablePromises. Observing cancellationIf a promise is cancelled, it needs to be observable. Yes, you don't want to do the same as "catch", but you often want to do "finally", as in stop spinners and other such UI. Say we had: var cancellablePromise = new CancellablePromise(function(resolve, reject) {
// Business as usual
}, {
onCancel() {
// Called when this promise is explicitly cancelled,
// or when all child cancellable promises are cancelled,
// or when the parent promise is cancelled.
}
});
// as a shortcut:
CancellablePromise.resolve().then(onResolve, onReject, onCancel)
// …attaches the onCancel callback to the returned promise
// maybe also:
cancellablePromise.onCancel(func);
// as a shortcut for .then(undefined, undefined, func)Usage in fetchFetch would return a CancellablePromise that would terminate the request onCancel. The stream reading methods If you're doing your own stream work, you're in charge, and should return your own CancellablePromise: var p = fetch(url).then(r => r.json());
p.abort(); // cancels either the stream, or the request, or neither (depending on which is in progress)
var p2 = fetch(url).then(response => {
return new CancellablePromise(resolve => {
drainStream(
response.body.pipeThrough(new StreamingDOMDecoder())
).then(resolve);
}, { onCancel: _ => response.body.cancel() });
}); |
|
Clearing up a question from IRC: var fetchPromise = fetch(url).then(response => {
// noooope:
fetchPromise.abort();
var jsonPromise = response.json().then(data => console.log(data));
});In the above, var jsonPromise = fetch(url).then(r => r.json());Now |
mariusGundersen
commented
Mar 26, 2015
|
Since calling abort might not abort the fetch (request), I don't think the method should be called var request = fetch(url);
var json = request.then(r => r.json);
var text = request.then(r => r.text);
text.ignore(); //doesn't abort the fetch, only ignores the result.This would also work well with promise implementations that don't support cancellations (like the current spec), since calling //doSomething does not return a cancellablePromise, so calling abort won't abort what is
//happening inside doSomething. ignore makes it clear that only the result will be ignored,
//any data done can't be guaranteed to be aborted.
doSomething().then(url => fetch(url)).then(r => r.json).ignore() |
If you call it on the promise returned by Your example will fail because you have two consumers of the same stream, we reject in this case. It should be: var requestPromise = fetch(url);
var jsonPromise = requestPromise.then(r => r.clone().json());
var textPromise = requestPromise.then(r => r.text());
textPromise.abort();In this case, I don't think "ignore" is a great name for something that has these kind of consequences. Maybe there's a better name than |
jyasskin
commented
Mar 26, 2015
|
@jakearchibald, in your proposal, it looks like Does any of this flow through to the I'm also worried about subsequent uses of the original promise hanging instead of rejecting. |
|
@jyasskin the refcount would be increased by cancellable promises. So If you use
Yeah, it should |
jyasskin
commented
Mar 26, 2015
|
'k. Say we have a careless or cancellation-ignorant library author who writes: function myTransform(yourPromise) {
return yourPromise
.then(value => transform(value))
.then(value => transform2(value));If we had On the other hand, in a var cancellationSource = new CancellationTokenSource();
var result = myTransform(fetch(..., cancellationSource.token));
cancellationSource.cancel();And the fetch would wind up rejecting despite the intermediate function's obliviousness to cancellation. The "revealing constructor pattern" is bad for cancellation tokens because it requires special infrastructure to be able to cancel two fetches from one point. On the other side, cancellation tokens require special infrastructure to be able to use one fetch for multiple different purposes. Either of Anne's solutions can, of course, be wrapped into something compatible with either |
martinthomson
commented
Mar 26, 2015
|
Another alternative: let abortFetch;
let p = new Promise((resolve, reject) => { abortFetch = reject; });
fetch(url, { abort: p }).then(success, failure);
// on error
abortFetch();That would make the activity on fetch dependent on a previous promise in a similar fashion (but more intimate) to this: promiseYieldingThing().then(result => fetch(url)).then(success, failure);I don't like the implicit nature of what @jakearchibald suggests here. |
getify
commented
Mar 26, 2015
|
TL;DR I would like to speak strongly in favor of the "controller" approach and strongly opposed to some notion of a cancelable promise (at least externally so). Also, I believe it's a mistake to consider the cancelation of a promise as a kind of automatic "back pressure" to signal to the promise vendor that it should stop doing what it was trying to do. There are plenty of established notions for that kind of signal, but cancelable promises is the worst of all possible options. Cancelable PromiseI would observe that it's more appropriate to recognize that promise (observation) and cancelation (control) are two separate classes of capabilities. It is a mistake to conflate those capabilities, exactly as it was (and still is) a mistake to conflate the promise with its other resolutions (resolve/reject). A couple of years ago this argument played out in promise land with the initial ideas about deferreds. Even though we didn't end up with a separate deferred object, we did end up with the control capabilities belonging only to the promise creation (constructor). If there's a new subclass (or extension of existing) where cancelation is a new kind of control capability, it should be exposed in exactly the same way as new CancelablePromise(function(resolve,reject,cancel) {
// ..
});The notion that this cancelation capability would be exposed in a different way (like a method on the promise object itself) than resolve/reject is inconsistent/incoherent at best. Moreover, making a single promise reference capable of canceling the promise violates a very important tenet in not only software design (avoiding "action at a distance") but specifically promises (that they are externally immutable once created). If I vend a promise and hand a reference to it to 3 different parties for observation, two of them internal and one external, and that external one can unilaterally call That notion of trustability is one of the foundational principles going back 6+'ish years to when promises were first being discussed for JS. It was so important back then that I was impressed that immutable trustability was at least as important a concept as anything about temporality (async future value). In the intervening years of experimentation and standardization, that principle seems to have lost a lot of its luster. But we'd be better served to go back and revisit those initial principles rather than ignore them. ControllerIf a cancelable promise exists, but the cancelation capability is fully self-contained within the promise creation context, then the vendor of the promise is the exclusive entity that can decide if it wants to extract these capabilities and make them publicly available. This has been a suggested pattern long before cancelation was under discussion: var pResolve, pReject, p = new Promise(function(resolve,reject){
pResolve = resolve; pReject = reject;
});In fact, as I understand it, this is one of several important reasons why the promise constructor is synchronous, so that capability extraction can be immediate (if necessary). This capability extraction pattern is entirely appropriate to extend to the notion of cancelability, where you'd just extract Now, what do you, promise vendor, do with such extracted capabilities? If you want to provide them to some consumer along with the promise itself, you package these things up together and return them as a single value, like perhaps: function vendP() {
var pResolve, pReject, pCancel, promise = new CancelablePromise(function(resolve,reject,cancel){
pResolve = resolve; pReject = reject; pCancel = cancel;
});
return { promise, pResolve, pReject, pCancel };
}Now, you can share the Of course this return object should be thought of as the controller from the OP. If we're going to conflate promise cancelation with back-pressure (I don't think we should -- see below!) to signal the fetch should abort, at least this is how we should do it. Abort != Promise Cancelation... Abort ==
|
jhusain
commented
Mar 26, 2015
|
Related: Composition Function Proposal for ES2016 Might be interested in the toy (but instructive) definition of Task and how it is composed using async/await. |
|
I don't want to deny anyone the ability to rant about |
function myTransform(yourPromise) {
return yourPromise
.then(value => transform(value))
.then(value => transform2(value));
}
myTransform(fetch(url)).cancel();This would:
This works as expected right? |
|
@getify I appeal to you, once again, to filter out the repetition and verbosity of your posts before posting, rather than all readers having to do it per read. I ask not only for others' benefit, this will also boost the signal of the point you're trying to make.
Yes, that would be a specific and intentional difference between cancellable promises and regular ones. I understand in great detail that you don't like that, but can you (briefly and with evidence/example) show the problems this creates?
If you don't want to vend a cancellable promise don't vend a cancellable promise. If you want to retain cancellability, vend a child of the promise each time. |
NekR
commented
Mar 27, 2015
|
@jakearchibald what is wrong with this way? var req = fetch('...');
// or req.headers.then(...)
req.response.then(function(response) {
if (response.headers.get('Content-Type') !== 'aplication/json') {
req.cancel();
}
return response.json();
});
req.addEventListener('cancel', function() {
// ...
});
// or Streams-like style
// closed/cancelled/aborted
req.closed.then(function() {
// ...
});Here |
getify
commented
Mar 27, 2015
|
Before I get to the other points you've brought up (I have responses), let me focus on and clarify just this one:
Let me try to illustrate my question/concern (and perhaps misunderstanding). Assume: var parent = new Promise(function(resolve){ setTimeout(resolve,100); }),
child1 = parent.then(function(){ console.log("child1"); }),
child2 = parent.then(function(){ console.log("child2"); });First, what happens here? parent.cancel();Do both If merely passing Now, what happens if instead: child1.cancel();Does that mean that |
|
@NekR that's already possible in Canary today thanks to the Streams API: fetch(url).then(response => {
if (response.headers.get('Content-Type') !== 'application/json') {
response.body.cancel();
}
});We can already abort the response, it's the request we can't abort. The only case this is a problem is when the request is particularly large, say you're uploading a large file. It's trivial to do what you're suggesting whilst still returning a promise. The question is whether there's a benefit in the return of // If @@species is a regular promise:
var fetchPromise = fetch(url);
var jsonPromise = fetchPromise.then(r => r.json());
// To abort the request & response:
fetchPromise.abort();
// If @@species is abortable:
var jsonPromise = fetch(url).then(r => r.json());
// To abort the request & response:
jsonPromise.abort(); |
WebReflection
commented
Mar 27, 2015
|
answering @getify from my POV: var parent = new Promise(function (res, rej, cancel) {
var cancelable = setTimeout(res,100);
cancel(function () {
clearTimeout(cancelable);
});
}),
child1 = parent.then(function(){ console.log("child1"); }),
child2 = parent.then(function(){ console.log("child2"); });Since you expose cancel-ability, you setup what should happen when you cancel so that Now, as quantum physics tough us that the world is not just black or white, we also need a canceled state ( IMO™ ) as a way to ignore or react. Let's be more pragmatic, as Jake suggested already before ;-) |
Well, you get var parent = new CancellablePromise(resolve => setTimeout(_ => resolve("Hello"), 100));
var child1 = parent.then(value => console.log(value + " world"));
var child2 = parent.then(value => console.log(value + " everyone"));If parent is cancelled before resolving, it doesn't get to provide the value the others need to compose their log messages.
This isn't the case in my proposal, you can add a cancel observer in the same way you observe fulfill & reject. This would allow you to stop a spinner, but not display an error message, as cancellation often isn't error worthy.
"child2" does get printed. The parent has a cancellable promise child count of 1 ( |
getify
commented
Mar 27, 2015
The problem here is differing perspective and actor. I get the desire to want to cancel the What I am objecting to is the perspective that the code that creates In For example, one observer might say "I only wait a max of 3 seconds for a response, then I give up", but another observer may have a longer tolerance and want the request to keep going for awhile longer. |
jyasskin
commented
Mar 27, 2015
|
@jakearchibald Oh right, refcount-starts-at-0 again. I think you're right. I think this all leads to the guideline that, if you have a |
NekR
commented
Mar 27, 2015
Reasonable. Ideally it should, but as we all see it's a bit hard to decide right way for it. This is why I thought about returning non-promise from |
I can't think of a case you'd want that behaviour but it's Friday and I'm way over my thinking quota for the week. If you don't want children to be able to cancel the parent, cast to a normal promise before returning. function nonCancellableFetch(...args) {
return Promise.resolve(fetch(...args));
} |
But it can't. |
martinthomson
commented
Mar 27, 2015
|
I haven't been convinced by this thread that we need to solve the general problem of transitive promise cancellation. That adds a lot of reference for a feature that will be rarely used. It's a poor analogy, but other multithreaded promise-like things don't have a generic cancellation. Read the c# documentation on the topic, for example. What is wrong with adding a simpler hook, solely for fetch use? |
martinthomson
commented
Mar 27, 2015
|
Sorry, reference should have been complexity. I blame my phone keyboard. |
getify
commented
Mar 27, 2015
|
Requoting myself:
I'm not suggesting: child1 = parent.then(..);
child1.cancel();I am suggesting: child1 = parent.then(..);
parent.cancel();By having the reference |
|
@getify yes that's bad form of the child1 code. It should be written like your first example. Of course, if the vendor doesn't want cancellation, they can cast to I'm not too worried about this. You can have multiple listeners to DOM elements that can also mutate then. World keep turnin'. |
If there's no agreement on chainable cancellation, we could go with a promise subclass with an Because this terminates the stream, it may be impossible for the reader (eg With cancellation in the chain, it means that: function higherLevelFetch(url) {
return fetch(url).then(transformSomeWay).catch(recoverSomeWay);
}...produces an abortable value automatically. We'd lose that if You also lose the "all children cancelled so cancel parent" behaviour which I think works well for multiple listeners to the same fetch, but maybe if we ever get such a thing fetch could adopt it. |
getify
commented
Mar 27, 2015
The concern is not about what the "child1 code" should do, it's about what the "child1 code" can do. As promises stand now, "child1 code" cannot affect "child2 code". That's by design, and it's a good thing.
I had a feeling this suggestion was coming soon. So, you're essentially saying: var p = fetch(..), p_safe = Promise.resolve(p);
// later
child1 = p_safe.then(..)
..Sure, that might work to isolate the capability, since var controller = fetch(..), p = controller.promise;
// later
child1 = p.then(..)The problem with having "controller" be a I'd call that pit-of-failure design. |
You might have gotten that feeling because I'd suggested it twice in this thread already. |
NekR
commented
Mar 27, 2015
Should it be the same as: fetch(...).then(function(response) {
setTimeout(function() {
response.body.close();
}, 100);
return response.text();
});I believe |
No, your call to cancel the stream would fail as |
martinthomson
commented
Mar 27, 2015
|
@jakearchibald, isn't it sufficient to cancel just the request? That is to say, once the fetch promise resolves, the abort would have no effect. The abort would simply cause the promise to reject with a new error type. In that case, consumers of the response stream aren't affected, because there is no response at the time that any abort is enacted. An abort on the response stream is sufficient to cancel the response once it starts. |
|
That would mean |
NekR
commented
Mar 27, 2015
That is weirdest behavior I ever saw -- you cannot cancel because you are doing request. |
|
Well, you aren't, .json() is. |
martinthomson
commented
Mar 27, 2015
That's OK. At that point, cancelling the stream should suffice. That can be mapped back into a cancellation of the response. I guess that you could say that it's a little janky that there are apparently two ways to cancel a request then, but I think that it maps nicely into the API. I see each as independently useful, even if both map into RST_STREAM or a connection close in the end. |
martinthomson
commented
Mar 28, 2015
|
Now that I've properly considered the upstream propagation of cancellations, I think that I've concluded that it's a real hazard. If the intent of the sequence is to produce just the final product of a promise chain, then that is the only case where this is OK. However, a promise chain can frequently have side effects that are important to the functioning of a program. Pushing a cancellation upstream risks a downstream consumer of a promise cancelling actions, often unbeknownst to them, in code for that they don't necessarily know the structure of. Consider: Obj.prototype.doSomethingInnocuous = function() {
if (this.alreadyRun) { return Promise.resolve(); }
return innocuousActivity(t).then(() => { this.alreadyRun = true; });
}
var commitFetch;
obj.doSomethingInnocuous()
.then(() => commitFetch = fetch(commitUrl, {method: 'POST'}))
.then(showResult);
cancelButton.onclick = e => commitFetch.abort();Here, you might admit the possibility that Obviously you can structure the code functions so that important cancellable activities are defended by having unreachable dependencies: var p = importantThing();
p.then(() => {}); // prevent a cancellation on timeout()
return p.then(() => {});Or something like that, but that sort of defensive programming is extremely non-obvious. I think that would make cancellation virtually unusable by virtue of creating a strong disincentive to use it for fear of the potential for collateral damage. |
jyasskin
commented
Mar 28, 2015
|
@martinthomson |
martinthomson
commented
Mar 28, 2015
|
@jyasskin I agree regarding the two vs. three state thing. I don't see people handling cancellation unless it causes rejection. I can see where @jakearchibald is coming from there, with the cancellation avoiding I acknowledge the bug in my example - it's been a very long IETF week - but will stick to my thesis here. This is a footgun. q: What happens to a |
Readable[Byte]Stream.cancel works only when the stream is not locked. As |
martinthomson
commented
Mar 28, 2015
|
@yukatahirano, I could live with that. |
I'm not opposed to cancellation resulting in rejection, as long as you can tell a cancellation-rejection apart from other rejections. fetch(url).then(r => r.json()).catch(err => {
if (!isCancellation) showErrorMessage();
stopSpinner();
}); |
NekR
commented
Mar 28, 2015
Yeah, every time on autocomplete when I saying to stream what I want response in JSON I do not want to cancel prev. request if user types one more letter. Just add third way to cancel |
mariusGundersen
commented
Mar 28, 2015
|
Sorry for jumping back into the discussion so much later.
This is exactly why I think it should be called an If you have a promise that you need to give to multiple consumers, then you should clone it, just like you have to clone the request/response of fetch. That can easily be done like so: let parent = fetch(url);
let child1 = parent.then(r => r);
let child2 = parent.then(r => r);
child1.ignore()//child2 will still resolve |
jyasskin
commented
Mar 28, 2015
|
@jakearchibald It's up to the producer that receives a cancellation to call |
martinthomson
commented
Mar 28, 2015
|
@jyasskin, I agree precisely regarding the error type. Using As for upstream cancellation, I'd like to expand on the virtues of the .NET I'm also concerned that unless all promises are cancellable we well generate surprisingly non-uniform behaviour. Take: fetch().then(x).abort();
y().then(() => fetch()).then(x).abort();That alone is surprising, since the second call fails. The actual results can be hard to predict if the call to |
|
@NekR the sarcasm isn't helpful. Also, for short responses sending abort messages can take a longer than just receiving the short response. However, I agree that from a developer point of view the request should appear cancelled. |
Hm, I think "cancel" is better than "ignore" as the latter doesn't suggest that the underlying operation may cease. Whereas "cancel" can be "cancel my observation" and "cancel the underlying behaviour". Anyway, I'd rather get consensus on the feature design before bikeshedding naming.
Yep, if you want to retain cancellation but vend a child promise, it's simply |
When calling |
WebReflection
commented
Mar 30, 2015
|
FWIW I've implemented a cancelable Promise playground which is already suitable for everything discussed in here: https://gist.github.com/WebReflection/a015c9c02ff2482d327e Here an example of how it works: // will be resolved
new Promise(function ($res, $rej, ifCanceled) {
var internal = setTimeout($rej, 1000);
ifCanceled(function () {
clearTimeout(internal);
});
})
// will be resolved without executing
.then(
function () {
console.log('on time');
},
function () {
console.log('error');
}
)
.cancel()
// will simply execute and resolve
.then(function () {
console.log('no time');
});The Promise is cancelable only if a function to describe how to cancel it is provided. This detail is hidden from the outer world. In this scenario/case it would virtually look like the following: function fetch(url) {
return new Promise(function (res, rej, ifCanceled) {
var xhr = new XMLHttpRequest;
xhr.open('GET', url, true);
xhr.send(); // and the rest of the logic
ifCanceled(function () {
xhr.abort();
});
});
}
// outside
var page = fetch('index.html').then(function (text) {
document.body.textContent = text;
});
// at any time later on, if needed
page.cancel().then(function () {
console.log('nothing happened');
});All involved promises will be silently resolved. The developer has the ability to react and it does not need to expose any cancel-ability if not meant. The design is backward compatible with current Promise specification, without even requiring a I'm not sure it's perfect, but it works already and it seems to be suitable for any sort of scenario. My 2 cents |
|
@WebReflection Thanks for doing this. var p1 = new Promise(function (resolve, reject, ifCanceled) {
var internal = setTimeout(resolve.bind(null, 123), 1000);
ifCanceled(function () {
console.log('been cancelled');
});
});
var p2 = p1.then(function (val) {
console.log('done1 ' + val);
}, function (err) {
console.log('error1');
});
var p3 = p2.cancel().then(function () {
console.log('post-cancel resolve');
}, function() {
console.log('post-cancel reject');
});
var p4 = p1.then(function(val) {
console.log('done2 ' + val);
}, function() {
console.log('error2');
});Logs: (live example)
It feels weird to me that |
WebReflection
commented
Mar 30, 2015
|
cancel actually cancels up to any promise that hasn't finished yet. The whole point/ease of Promises is the Cancel in my example is the "reset" point and from then on you can use The main misunderstanding I see in your code is that you used Basically that does not work as resolve and reject, that is an optional way to provide cancelability via a function that is responsible to cancel. You are not clearing the setTimeout in there so you provided a callback that does nothing. Basically you created a cancelable Promise that won't cancel a thing, hence my design that requires you actually do cancel for real because once you've canceled, the Promise is canceled: meaning it cannot possibly be resolved or rejected, it's done, and resolved to avoid the forever pending problem. It's like invoking From the internal Promise world, it's up to you to provide a way to be canceled externally, but you actually delegate the outer world to invoke the You might also want to cancel from the internal world, without having any side-effect of the unknown uter world, hence a way to retrieve the No other value than Again, the key here is backward compatible, and the third argument is not the one that resolves or reject, but the one that explicitly expose the ability to be canceled. This is hidden from the outer world, but only if defined, the promise can be canceled: it would throw otherwise (know what you are doing and know what to expect) |
WebReflection
commented
Mar 30, 2015
|
as extra clarification, this is how you should write that var p1 = new Promise(function (resolve, reject, ifCanceled) {
var internal = setTimeout(resolve.bind(null, 123), 1000);
ifCanceled(function () {
clearInterval(interval);
console.log('been cancelled');
});
});How else could you possibly cancel that promise that would like to resolve in a second? That's the way, if you want resolve to happen then don't setup cancel-ability through the |
I understood. It's the same as the
Yeah, I'm increasingly convinced that I still like the ref-counting though, as a way to ensure a child cannot break an independent branch of the promise chain. |
WebReflection
commented
Mar 30, 2015
here's the catch: the code that is expecting a value will never be executed once the promise is canceled. It will be silently resolved and ignored for that "predefined time-lapse" that will be resolved and never executed. Rejecting in my opinion does not really reflect the nature of cancel/ignore intent, but if that's the road then my code becomes way simpler. Also, rejecting without a reason might be indeed a similar indication that it wasn't a real error but something "ignorable" Last on ref counting, there's no way my code won't resolve and having silent resolution kinda ensures non breaking branches. However, since the root is only yes/no and quantum cancel state is not easy to integrate on top of these basis, I start thinking that rejecting might be a better approach. I might write a different snippet based on such behavior and see how it goes, I still believe the callback to define how to cancel should be private, optional, and eventually defined at Promise initialization time. |
Agreed |
mariusGundersen
commented
Mar 30, 2015
Sure, I won't bring up naming again.
The problem I see is that the next step in the chain might expect something, for example: fetch(url)
.then(r => r.json())
.cancel() //cancel, the above line resolves to undefined
.then(json => json.something) //this line rejects with an error, because json is undefinedThe same applies to rejecting with undefined: fetch(url)
.cancel()
.catch(logErrorToSomewhere);This will log a lot of |
WebReflection
commented
Mar 30, 2015
|
If you cancel you don't expect anything because you canceled. Going through Agreed about the catch concern, which is why I went for a silent cancelation. If you cancel, whoever was interesting in the promise result will never be executed (but all Promises resolved regardless, no side-effect there, that's the goal of my design) Who canceled, could still do something with that promise, if needed, just to follow the pattern, not because it will exect a value. However in my initial design |
mariusGundersen
commented
Mar 30, 2015
|
My example was greatly simplified, it's more likely to look something like this: inputElement.onkeyup = function(){
search.autocomplete(this.value).then(updateAutocompleteListWithResult);
}
let search = {
active: null,
autocomplete: function(string){
if(this.active) this.active.cancel();
this.active = fetch(`/autocomplete?q=${string}`)
.then(r => r.json())
.then(r => (this.active = null, r), e => (this.active = null, e));
return this.active;
}
}In this scenario I'm not interested in the result of the autocomplete fetch if a new key up event occurs before the results are ready. In this scenario I'm not interested in neither the resolved nor the rejected value from autocomplete; I don't want |
Well, because of the scoping of your cancel function (which looks weird but is great in practice), you have easy access to |
WebReflection
commented
Mar 30, 2015
|
My code will not invoke that indeed if canceled. My code silently resolve,
|
WebReflection
commented
Mar 30, 2015
|
It does make sense to me but I've created something I had to explain
|
mariusGundersen
commented
Mar 30, 2015
Aha, I've probably misunderstood you all this time. It sounds like we both agree that the state of the cancelled promise should be "cancelled", rather than "fulfilled" or "rejected". I also think it should not call new CancellablePromise(function(resolve, reject, isCancelled){
isCancelled(() => reject("cancelled"));
})But I don't see how that would work with reference counted cancelling, which is an idea I quite like. |
WebReflection
commented
Mar 30, 2015
|
I think you keep misunderstanding my proposal and code. You don't want to expose the ability to resolve or reject, however, you might want expose the ability to cancel on;y if you provide a way to do so rejecting inside a cancel makes no sense to me, if it's canceled, it's canceled meaning, indeed, not resolved, neither rejected. Using your snippet you'll end up handling the error per each new autocomplete request. You don't want to do that, you want that nothing happens, you cancel, and you assign a new fetch. My code provides such ability internally creating a cancel state. Jake idea was that if silently resolved with undefined, maybe we could actually pass instead a value so that you can |
getify
commented
Mar 30, 2015
|
While this thread is about If we don't consider both concerns together (or rather, how/if to pair promise observation with upstream cancelation signaling in general), rather than only narrowly thinking about What's being currently discussed would mean that somehow an async function fetchLike(url) {
var response = await ajax(url);
return response.text;
}
fetchLike("http://some.url.1").then(..);Since the ultimately returned promise is implicitly created by the |
WebReflection
commented
Mar 30, 2015
|
FYI I've updated the code so that you can provide a value when canceled. // will be resolved
new Promise(function ($res, $rej, ifCanceled) {
var internal = setTimeout($rej, 1000);
ifCanceled(function () {
clearTimeout(internal);
});
})
// will be resolved without executing
.then(
function () {
console.log('on time');
},
function () {
console.log('error');
}
)
.cancel({beacuse:'reason'})
// will simply execute and resolve
.then(function (value) {
console.log(value);
});Invoking cancel again can be done internally ( @getify I'm using timers and events to test this stuff, I don't even care much about fetch itself in terms of solution. fetch is just Yet Another Case when you want to cancel something at any time. |
WebReflection
commented
Mar 30, 2015
|
providing a canceling mechanism is the only way to go: either (optionally) internally (and that's my favorite, nothing awkward here since it's internally that you resolve or reject) or trough a controller. Passing a controller around together with a promise in order to cancel seems dumb to me, if you always need boths whhy not just passing a promise with If you dont' want any of them why not passing a cancelable promise inside a promise so that no cancelability will be exposed ? The async function fetchLike(url) {
// where is the catch?
// how do you catch?
var response = await ajax(url);
return response.text;
}
fetchLike("http://some.url.1").then(..);However, if indeed a value is expected, your example will be as easy as hell to go with I also believe if await should do exaclty what Promises do, then we have a redundant pattern impostor |
getify
commented
Mar 30, 2015
Right, but I think you missed my point, which is that That's the major weakness of your idea, IMO, that it only works for explicit promise-creation tasks, but doesn't seem to work for implicit promise-creation tasks.
If you're talking about canceling
That's precisely the point I've made many times in this thread, that you don't always need both, and in fact it's dangerous to system trustability to always have both. The advantage of the controller with separate observation and cancelability is that in places where you need both, you pass the controller, and in places where that's a bad idea, you pass only the promise (or only the cancelation). |
getify
commented
Mar 30, 2015
I think you're suggesting this: async function fetchLike(url) {
try {
var response = await ajax(url);
return response.text;
}
catch (err) {
// ..
}
}
fetchLike("http://some.url.1").then(..);Of course, you can do that... but the |
jhusain
commented
Mar 30, 2015
|
It is precisely because of the limitations of promises that there is currently a proposal to replace async/await with a more extensible syntax that can apply to other types such as as .NET-style Task. Providing syntactic support for promises encourages people to use them where they are inappropriate. Fetch is an excellent example of an API that should not use Promises, because it involves the use of a scarce resource (ie connections). Promises are a very well articulated concept. They are the asynchronous equivalent of a value returned by a synchronous function. When you call a function synchronously it cannot be canceled. Rather than try and evolve a Promise into something else, or inappropriately use it in order to get better language support, why not invent a new type that is just as compositional as a promise, but has the required semantics for fetch? A reference-counted Task provides the necessary semantics for fetch. Rather than providing a guarantee of cancellation, you can simply give the producer the ability to determine whether anyone is listening for the outcome of an asynchronous operation. If all consumers stop listening for the outcome, the producer can have the opportunity to cancel the operation because it is not observable. In my opinion this is the global maximum, because consumers do not have to concern themselves with cancellation errors. The request can only be canceled if there are no listeners, which means that when the request is finally canceled, no one is around to hear it. Reference counting also ensures that consumers do not need to be made aware of other consumers. This approach works very well for Netflix which often uses ref-counted scalar Observables of 1 to represent async operations. We would not use a Promise for asynchronous requests in the browser because there are too many UI interactions that require rapid creation and cancellation of pending data requests (autocomplete box being the most common). JH
|
getify
commented
Mar 30, 2015
The problem I see with this implicit GC-directed version of cancelation is that the producer very quickly stops being in control of who is observing the outcome. If you make a Also, GC is not guaranteed to happen as soon as all refs are unset. If you want to abort a |
WebReflection
commented
Mar 30, 2015
and that's my point and implementation too. You expose the cancel-ability only if you define a method to cancel. Actually my code goes further than that, if you don't internally define a method to cancel and you use The implicit cancellation suits perfectly My explicit promise creation is like that because as a user, I want to be also able to create cancel-able Promises and there it goes: if core or external APIs provides cancel-able promises, you can cancel them ... otherwise you cannot, as easy as this sound. The In this case the new Promise(function ($res, $rej, ifCanceled) {
var internal = setTimeout($rej, 1000);
ifCanceled(function () {
clearTimeout(internal);
});
})
.then(
function () {
console.log('on time');
},
// we'll end up here
function () {
console.log('error');
}
)
.cancel({beacuse:'reason'})
// never executed
.then(function (value) {
console.log(value);
});If you are careless about errors, this might work ... but ... I honestly prefer my first idea, creating a mark-point in the chain where everything happened before will be ignored but whoever cancel has the ability to react after, eventually providing the resolution. Anyway, we have 2 playgrounds now. |
jhusain
commented
Mar 30, 2015
|
This is a very reasonable concern in principle, but in my experience is not a problem in practice. Perhaps this is a matter of not clearly defining what I mean by producer and consumer. In this context by producer I mean "fetch" which controls the code which backs the Task. By consumer I mean the code that assigns a callback to a Task and receives a subscription which can be used to stop listening for the result: var task = fetch(...); var subscription = task.get(value => console.log(value), error => console.error(error)); // subscription.dispose() can be used to stop listening In this context the producer (fetch) does not cancel unless there are no more consumers. Fetch doesn't have sufficient context to cancel a request. As for garbage collection, disposing of the subscription removes the reference from the task to the handlers passed to get. This breaks the link and allows GC to occur. Can you clarify what you mean by producer? All application use cases I have encountered in UIs can be accommodated with the notion of consumer unsubscription. An event may occur which may cause a consumer to stop listening for the result of a task, such as a form being closed. In these situations it can be the consumers responsibility to explicitly unsubscribe from a Task when events occur which cause their eventual data to become irrelevant. This can even be done declaratively with compositional functions as in the example below: var task = fetch(...); var subscription = JH
|
WebReflection
commented
Mar 30, 2015
|
Agreed that ref count would be problematic for few reasons:
In any case, this perfectly summarizes my general disappointment with Fetch
But since we are here ... I guess it's too late so let's try to be pragmatic and cover as many cases as possible, keeping in mind the initial ease goal that was fetch. Shall we? |
getify
commented
Mar 30, 2015
Since By "producer" I really mean the code which actually makes the initial call to the But this "producer" code also may very well send that promise to other parts of the system, either directly, or indirectly as a sub-promise chained off the main returned promise. It's these other parts of the system which would have reference (refcount) that the main "producer" would not be able to undo if it later needed to trump the system and say "hey, gotta abort." I regularly model my systems where I take a single promise from some util and pass it around to various observers in the system as a token to let them know "it's ok to move on with what you were doing" (like a really lightweight event subscription). But I also have cases where the initial "producer" of that promise needs to take over and yell "stop the presses!". It's much harder to design such a system if I also need all those observer parts of the system to expose some |
jhusain
commented
Mar 30, 2015
|
I believe it is much easier (and more desirable) to add a version of fetch that returns a Task than it is to change the definition of Promise. I'm afraid I've been unclear about what I mean when I say reference counting. What I really mean is subscription counting. Every task can keep a simple counter of the number of subscriptions that exist. This mechanism is totally outside of the garbage collector and implemented in plain old JavaScript. Because it is necessary for each consumer to explicitly unsubscribe, there is a clear opportunity to decrement the counter. The Task I propose would have a "then" method which would have the same signature as a promise. However it would also be lazy and also have a get method which was used to actually retrieve the data. "Then" would not trigger the request, just create a new Task. The get method would be used to actually retrieve the data. You can see an example (minus the reference counting) here: https://github.com/jhusain/compositional-functions/blob/master/README.md Now subscription counting is simply a matter of counting the number of get calls in the system, not the number of references to the Task. This is how the Observables in Rx work, using flatMap for Compositon and forEach for consumption. JH
|
getify
commented
Mar 30, 2015
Unless I've missed something, I'm not sure how that changes any of my assertions about the difficulty that such things incur when you've passed this token (call it a "Task" or "promise" or whatever) around to multiple different parts of a system for observation. That's especially true if one of the places you pass such a token to is an external part of the system (a third party lib, etc), where you cannot even change their API to allow for explicit unsubscription. |
WebReflection
commented
Mar 30, 2015
|
I've got the feeling that the moment we use But AFAIK not using Promise wasn't an option ... now I've provided 2 playgrounds so I stop making noise. |
|
Say you have a ServiceWorker with: self.addEventListener('fetch', function(event) {
event.respondWith(
fetch('/whatever.json').then(function(response) {
return response.json();
}).then(function(data) {
return fetch(data.url);
})
);
});Say this request is in progress and the user hits X in the browser UI, closes the tab, or navigates. The browser can cancel the stream once the promise it gets resolves, but it cannot abort either fetch or the A chaining cancellable promise solves this problem, as the browser calls cancel, and the currently in-progress part of the promise chain is terminated. Counter to some of the complaints in this thread, this is a case where you do want the receiver of the promise to have cancellation rights. |
getify
commented
Mar 30, 2015
There are definitely places where you want the cancelation capability to be propagated. No question. There are definitely others where you don't. Composing observation/cancelation means you don't have this choice. The choice is critical. |
WebReflection
commented
Mar 30, 2015
|
So why my proposal that exposes cancel-ability only if internally provided wouldn't work, exactly? I'm not sure I follow the problem. There are places where you need/want to expose cancel-ability, you go for it. Is this too KISS? |
getify
commented
Mar 30, 2015
You never addressed my earlier concern that a general implicitly-creating-promise type mechanism, like
|
wanderview
commented
Mar 30, 2015
There are probably other places where this same sort of thing applies, but for this particular case the browser has other mechanisms to cancel these operations. At least, in gecko we do. |
WebReflection
commented
Mar 30, 2015
|
The await mechanism should silently return without keeping executing the awaited function. Same as I silently resolve all Promises in the chain. Does this answer? You are also asking returns for Generators, right? That's the idea |
getify
commented
Mar 30, 2015
No, it doesn't answer my concern. The promise returned externally by the Specifically, how will you conditionally decide to tell this function's promise that it should be of the cancelable kind? async function foo() {
// .. what do we do here?
}
foo().then(..).cancel(); |
WebReflection
commented
Mar 30, 2015
|
It's not fully specified yet so I'd say it would work similar as generators do ... you externally invoke If you have an My proposal works well with cancelable things if these are cancelable, otherwise it throws regardless. How to know if an API is cancelable? Same way you know you can use any method from any kind of known API/object. About I might miss something about your concern, or maybe over-simplify something I don't see as concern. The road otherwise is to have Promises problems in ES7 too ... that does not sound brilliant to me. |
getify
commented
Mar 30, 2015
Where do you invoke this? On the return value from an It's also not clear how that addresses what you're talking about, or answering my direct question: In your view of cancelable promises as being decided based on what you pass into the promise constructor, how do you do that when the promise is constructed implicitly behind the scenes? |
WebReflection
commented
Mar 30, 2015
|
The .next was compared to .cancel as method you invoke outside. Calling
|
getify
commented
Mar 30, 2015
Sorry, I'm still lost. Perhaps you're missing the fact that the promise which comes back from an
There is no such thing. You don't call We're obviously bogging down this thread and way off track. If you'd like to explore |
NekR
commented
Mar 30, 2015
|
It's really hard to follow all the things here by now. But can we all agree at least of basic concept things before actual implementation details?
Any comments about these things? I would like to hear concept comments, where I was wrong, etc. No implementation stuff. Once we have final conclude on concepts, then we can discuss actual implementations. |
getify
commented
Mar 30, 2015
You purport to describe things in basic concepts, but then you choose a specific solution (promise instance cancelation) for all your assumptions. I object. Not that my objection carries any weight. My summary of "basic concept things" that seems more sensible and less limiting would be:
|
NekR
commented
Mar 30, 2015
|
Just to be clear @getify, I am not talking here about "concepts" in general, but rather about "implementation concepts" and not "implementation details of those concepts". Main goal of this is to stop flying around few differing concepts arguing with implementations details, many heuristics, etc. We need to choose something already here, otherwise it will never end. So, let's me comment your items, but please, do not go into "I believe this is concept, but this is not". Let's agree on things which easy to decide (some high-level concept over upcoming implementation of them).
This is exactly that thing about I am talking. We flying around few concepts of should it be promise or not, chainable or not, blah or not-a-blah. We need to decide and then go on.
This is implementation detail about how it should be chainable. I am ask about agreement what it should "chainable", implementations things "how" will follow later.
Cannot understand this, but please do not continue.
I believe this is same as I wrote in my last item, but just in other words. I can live with this sentence if this makes you more comfortable. |
WebReflection
commented
Mar 30, 2015
If we have cancel-able Promises in place for ES7, the external one you mention will be implicitly created by the JS engines as cancelable so that whatever is on hold internally through I really don't have any better way to explain this, but you should really try to open your mind 'cause of course this pattern is not possible yet, which is why we are here: to improve, not to re-iterate the already uncancel-able idea behind. Accordingly ....
There could be such a thing if we move on, so that you can have cancel-able Promises and everything I've already coded already works and makes sense.
I've never even brought them up so you should really probably discuss them somewhere else, if you are still confused by my Cancelable Promise solution that integrates perfectly with something not even standard yet: We must be able to fix things before these are out, not be stuck with documentation about partially defined standards. Agreed? I hope so ... |
WebReflection
commented
Mar 30, 2015
|
@NekR I'm honestly off philosophy because we have a real need and clock is ticking. I've created 2 playgrounds few here keep ignoring: one that cancels through reject and it ends up in the first catch instead of keep going as a cancel behavior I'd expect, and one that silently resolves everything without executing a thing until the cancel and provides a way to resolve with arbitrary data from that point on. More convolute in terms of specs, but the best/surprise-free behavior I could imagine. Both woudl work with async and await, as long as we consider async and await not finalized standards. |
petkaantonov
commented
Jan 30, 2016
It's ok for the sole "owner" of the promise to cancel the promise for its clients. The clients are not "co-owners" of the promise. |
benjamingr
commented
Jan 30, 2016
I think I might have miscommunicated. Your example absolutely does not leak a reference with refcount semantics. My note was with regards to unhandled exception tracking and its relation with cancellation. See https://jsfiddle.net/v6LrLdsj/ var p = cancellablePromiseFactory();
p.catch(() => {});
p.cancel(); // p gets cancelled hereOn the other hand: var p = cancellablePromiseFactory();
var p2 = p.then(...);
var p3 = p.then(...);
p2.cancel(); // refcounting semantics means it's unsound to abort hereExample: https://jsfiddle.net/1vfq7oq1/ I assumed you tried these semantics out first before you engaged in this debate. Feel free to use the fiddle links as a playground for testing out bluebird's cancellation. |
benjamingr
commented
Jan 30, 2016
I don't believe so, RxJS has multicast semantics too for when you want to share that value, when you do that you almost always use refcount semantics for the cancellation - so much that The only way to deal with multiple subscribers deterministically (without reliance on GC) in a sound way for explicit cancellation is to ref-count the number of subscribers - just like Rx does. The main advantage of a task is that it would not be shared by default and it can for example avoid caching the value. It also means that it would have simpler cancellation but that a task can no longer be cancelled if/when converting it to a promise. A task would also presumably have to That said, again, we have sound promise cancellation today - so while tasks are a very interesting concept to explore given the current state of Tasks and promises have strict semantics where creating the task launches the operation since they are proxies for values and not actions. Observables have ultra-strict semantics but only after you subscribed to them. To clarify - an observable is like a function, a promise is like a function's return value - you compose and combine promises through composing and combining regular functions. You compose and combine observables through lifted operators.
Petka already answered that, but feel free to look at the jsfiddle examples I posted in my above comment that show that this is a non-issue. |
jan-ivar
commented
Jan 30, 2016
@benjamingr thanks for the fiddle, but I must not be communicating well either. Here you're cancelling at the intersection point. Of course that works. The problem is downstream. In real code there are always reasons to extend a promise chain before returning it, as in fact is the case in the live code I linked to. So my concerns stand. Try https://jsfiddle.net/xev0n48y/
|
davidbonnet
commented
Feb 1, 2016
|
If this helps, we are currently using in production a fork of fetch() that adds an // Creating a request instance
var request = fetch(url, params);
// Calls to request.then() return the request object and internally update the promise
request.then(doSomething);
// Calls to request.abort() simply abort the request
request.abort();No need to update promises… |
WebReflection
commented
Feb 1, 2016
|
@davidbonnet you forked a polyfill. As soon as a browser with native fetch support lands in one of your projects you can say goodbye to them. Please let's try to keep polyfills a part this discussion which is already a never-ending-story through regular standards process, thanks. |
jan-ivar
commented
Feb 1, 2016
|
@WebReflection I take @davidbonnet 's point to be that we're over-designing and getting stuck: Have |
davidbonnet
commented
Feb 1, 2016
|
@WebReflection Indeed, we morphed the polyfill into a module. Otherwise, as you mention, our custom The point is to show an actual use case to help find a solution for this feature. |
WebReflection
commented
Feb 1, 2016
|
yes @jan-ivar the only thing needed was a special case/override of the returned object with a It could have been a 10 minutes task as generic library PR/fix and yet, after a year, there's no way to do that because the solution should be "universal". I, myself, changed my mind about Promises and realized they are just perfect for what they do and I don't care about their cancellability. What I care about is chosing Promise when Promise is the best choice for whatever new API will go out in the future ... and not as mandatory, unique, possible, solution to everything asynchronous. In this fetch case, returning directly something un-cancelable, Promise wasn't simply the best choice and the fact in over a year we are still discussing how and where and if we should cancel it fully demonstrated it wasn't the best pick. I hope these mistakes won't be repeated in the future, but for now ... long live to libraries able to fix this ... as long as these are libraries and not polyfills. Polyfills are about standards, bringing an Best Regards |
WebReflection
commented
Feb 1, 2016
|
@davidbonnet there are plenty of working examples, solutions, and use cases in this thread already. Keep being notified after a year with something already discussed almost daily made me come back to clarify that we all know how to fix this, it's just a matter of: "will it scale universally as solution? will it cover them all?" I wish these conversations were done upfront, and not after a standard ... yet I understand the bikeshedding would be close to impossible to handle. I honestly also think this should be closed because if in a year there's no solution it means there shouldn't be. Maybe in the future there will be a lightweight fresh new The API could be as simple and straight forward with same fetch API signature but its returned object is not a Promise, just a wrap of a Promise with an internal const request = (...args) => {
let dropped = false;
let f = fetch(...args);
let c = r => dropped ? null : r;
let p = f.then(c, c).catch(c);
return {
drop: () => dropped = true,
then: (...args) => p.then(...args),
catch: (...args) => p.catch(...args)
};
};If that's even an option I hope this thread will be closed. Best Regards |
benjamingr
commented
Feb 2, 2016
That's not the use case you describe in the comment you linked to though - it fixes that. I think it would be helpful to look at more code - got any motivating examples where cancellation wouldn't work? There's #27 (comment) but that's obviously not a very good example since bluebird semantics would work there. A bigger issue is that it contains incorrect code and has an inherent race condition for caching a flag and not a promise, more than one invocation will call Obj.prototype.doSomethingInnocuous = function() {
if(this.p) return this.p;
return (this.p = innocuousActivity(t)); // cache the promise, not the value
}Now, let's argue for a minute "but people can still write code that looks like the original example" AND fork one level up and not cancel the actual Obj.prototype.doSomethingInnocuous = function(t) {
if (this.alreadyRun) { return Promise.resolve(); } // OK with race condition.
return fetch(commitUrl, {method: 'POST'})).then(x => this.alreadyRun = true);
}
var commitFetch = obj.doSomethingInnocuous()
.then(showResult);
cancelButton.onclick = e => commitFetch.abort();That would still work since there is only one subscriber - let's see if we can contrive it a little further: Obj.prototype.doSomethingInnocuous = function(t) {
if (this.alreadyRun) { return Promise.resolve(); } // OK with race condition.
var wizard = fetch(commitUrl, {method: 'POST'}));
wizard.then(x => x.alreadyRun = true); // contrive it by forking the chain for side effects
return wizard.then(x => x); // again, forking here is contrived, but let's do it anyway.
}
var commitFetch = obj.doSomethingInnocuous()
.then(showResult);
cancelButton.onclick = e => commitFetch.abort();Here we have contrived the use case enough for cancellation to not work. Indeed this can be a problem. The code in doSomethingInnocuous would have to call No let's say for a second this more contrived use case is plausible in real code - this is still way way safer than abort semantics - with abort semantics both ends of the fork would get rejected and you have an unhandled error condition you're never dealing with and an error to the console - code dealing with a network error and retrying the fetch for example would run (since On the other hand, a simple warning (like with unhandled promise rejections) can solve this pretty nicely - although in all honesty I've never had issues with cancellation counting semantics. |
benjamingr
commented
Feb 2, 2016
|
@WebReflection I don't understand. We figured out sound promise cancellation semantics that work and are sound - we didn't a year ago, our understanding of how to deal with promise cancellation should work got a lot better during that time. A year ago I was (happily) using tokens which I barely touch now. It makes absolute sense to use promises for |
WebReflection
commented
Feb 2, 2016
|
sorry @benjamingr but I'm lost in this thread and if there's a solution I don't understand why this is still open. Since there are devs raising things discussed a year ago I thought it went nowhere. Glad to know there is a solution, can't wait to see it live. |
benjamingr
commented
Feb 2, 2016
|
@WebReflection we don't have an accepted solution for fetch. I'm just saying it's not like this has been open for a year and no progress was made in the front of cancellable promises. It's something we (at least I) didn't really understand a year ago and now we have pretty decent semantics that work well in practice in bluebird. Whether or not @annevk / @jakearchibald / spec people decide to adopt sound promise cancellation semantics is entirely up to them :) |
|
Right, there is a plan to start standardization work on cancelable promises and the idea is to use the outcome of that for |
jan-ivar
commented
Feb 2, 2016
@benjamingr It absolutely is, and it doesn't fix it. I must not have communicated well in #27 (comment). Let me try again: let operations = Promise.resolve();
function chain(func) {
let p = operations.then(() => func());
operations = p.catch(() => {});
return p;
};
let someCleanup = () => { /* doesn't matter what */ };
let fooImpl = x => new Promise(resolve => setTimeout(resolve, x));
let foo = x => chain(() => fooImpl(x)).then(() => someCleanup());
let p = foo(3000).then(() => console.log("here")).catch(e => console.error(e));
p.cancel();Here one would expect |
petkaantonov
commented
Feb 2, 2016
|
The owner of the promise is willfully making the promise uncancellable, working as intended |
jan-ivar
commented
Feb 2, 2016
|
Fine: let p = impossibleToIgnoreCancellableFunction()
.then(() => foo(3000))
.then(() => console.log("here"))
.catch(e => console.error(e));
p.cancel();
|
petkaantonov
commented
Feb 2, 2016
|
Those .then() and catches() will not be called, but whether the actual operation will be aborted depends (as it should) on the owner of the promise. |
jan-ivar
commented
Feb 2, 2016
|
@petkaantonov Do you agree the above will never cancel |
petkaantonov
commented
Feb 2, 2016
|
I don't understand what that means. The caller here will have achieved everything they need, that their callbacks are not called because they were cancelled. Whether the operation itself is aborted should not be their concern, but it should be aborted if |
eplawless
commented
Feb 2, 2016
|
@petkaantonov I think this might help clear up some of the confusion. The other interpretation is that |
petkaantonov
commented
Feb 2, 2016
|
but because the .catch is the only client of the .then before the catch, that .then will be cancelled. And because that then is the only client of the then before that then, that then will be cancelled. And so on. |
eplawless
commented
Feb 2, 2016
|
yep, understood. it's down to whether that's the desired semantic. I couldn't say one way or the other. |
jan-ivar
commented
Feb 2, 2016
|
@petkaantonov Sounds like you get it will never cancel |
benjamingr
commented
Feb 2, 2016
Half the point of disinterest semantics is exactly to avoid that action at a distance. I'll respond to your earlier response later, sorry, kind of busy but am interested in keeping this discussion going. |
jan-ivar
commented
Feb 2, 2016
|
Imagine Independent of this argument, I think it's a fallacy to suggest that clients only care about their callbacks. This assumes functions without side-effects. |
benjamingr
commented
Feb 3, 2016
No, I don't understand - why? If someone changes the code at a distance to behave differently of course you get action-at-a-distance. The point is that the semantics of your code don't change, the same paths still get executed regardless of how the code was changed at
As far as I understand no one is suggesting that though. . |
jan-ivar
commented
Feb 3, 2016
Assume that The client does not care exactly how Blaming the implementation of Worse, you still haven't told me how
The client intended for |
srcspider
commented
Feb 15, 2016
|
While I've skimmed though the discussion, so sorry if I somehow missed this being discussed already (I've only seen brief mentions of similar suggestions). What's the problem with ignoring the Promise altogether? Example // Normal use case. Not possible to stop.
var p = fetch('http://someplace'); // => Promise// When you need to be able to stop it you do this.
var fh = fetch('http://someplace', { returnType: 'handle' }); // => FetchRequestType
var p = fh.promise(); // => Promise (equivalent to normal fetch)
// "May" stop the request in which case the promise will be dropped. There is no error
// or resolution, the promise just never resolves or errors out. If content is already
// being streamed in, then it does nothing and just lets it finish.
fh.deprecate();The // Same as no options, but forcefully interrupts content streaming. Whatever you got
// so far becomes last byte sent. User is responsible for handling the partial data.
fh.deprecate({ bodyInterrupt: true });
// Same as no options but forces Promise error handler if fetch decides to abort.
fh.deprecate({ onHeaderAbortTriggerError: someAbortError });
// Same as above only trigger success instead of error if fetch decides to abort.
fh.deprecate({ onHeaderAbortTriggerSuccess: someAlternativeSuccessBody });
// ...whatever other options people wantDeprecating a request would not guarantee an Ignoring the promise should work with current promise implementations just fine, but it would probably be ideal for If this still results in "complications" then, as suggested earlier, ideally just make it so once user asks to work with "cancellation" the concept of getting a promise is just thrown out the window. Promises may be new and shiny and solve a lot of problems, but there's no reason to force ALL problems on them. |
benjamingr
commented
Feb 16, 2016
|
@srcspider you're describing what I'm suggesting exactly - disinterest semantics. http://bluebirdjs.com/docs/api/cancellation.html I still have to respond to @jan-ivar I'll try to do it sometime next week. |
srcspider
commented
Feb 17, 2016
|
@benjamingr awesome! |
This was referenced Feb 29, 2016
pyryjook
commented
Jun 21, 2016
|
What is the status of this suggestion? |
SaschaNaz
commented
Jun 21, 2016
|
@pyryjook Cancelable Promise ES proposal will cover this. |
jan-ivar
commented
Jun 21, 2016
|
@benjamingr You said you'd respond to my criticism of this idea in #27 (comment), and even going back earlier to #27 (comment), but that was 4 months ago, so I assumed this terrible idea was dead. I was apparently wrong. Have you had time to ponder what I'm not seeing? |
benjamingr
commented
Jun 21, 2016
|
@jan-ivar the repository is called Cancelable promise but it's not actually about promise cancellation. It's about third state semantics. What we're looking at is https://github.com/zenparsing/es-cancel-token Honestly I still think that promise cancellation works just fine and so do thousands of others who use it in production (no action at a distance). Apparently the language TC wanted to get things to a point where everything is cancelled the same way (through cancellation tokens). So we're very likely to see:
Where you cancel the token and the cancellation capability is passed separately. Please do drop by https://github.com/zenparsing/es-cancel-token and leave feedback - lots of ideas being tossed around. As for https://github.com/domenic/cancelable-promise it deals with what happens after the token is activated - we need cancellation semantics that are not abortive semantics so in a way we both win in some parts. I get non-abortive sound cancellation semantics and you get tokens instead of |
benjamingr
commented
Jun 21, 2016
That is not correct; it subsumes both. The es-cancel-token repo serves as inspiration but is not currently under consideration by the committee. |
benjamingr
commented
Jun 21, 2016
|
@domenic oh, I did not understand that, when did this change? Was there a June meeting or something? |
|
es-cancel-token was never presented to the committee, so I don't think there's been any change. I presented a proposal which was inspired by es-cancel-token and is being developed in domenic/cancelable-promise. The current spec under consideration for cancel tokens is https://domenic.github.io/cancelable-promise/#sec-canceltoken-objects |
benjamingr
commented
Jun 21, 2016
|
Oh, I saw you linked to es-cancel-token in your presentation so I assumed so. Anyway, from what I understand directly cancellable promises are off the table - is that not the case? |
|
Assuming you mean adding a |
benjamingr
commented
Jun 21, 2016
|
Thanks, then @jan-ivar please provide feedback in https://github.com/domenic/cancelable-promise :) |
jan-ivar
commented
Jun 21, 2016
|
I left comments in domenic/cancelable-promise#25 though it was closed as out of scope. PTAL. No better place for general discussion it seems. |
sleepycat
added a commit
to sleepycat/mapbox-gl-geocoder
that referenced
this issue
Jun 28, 2016
|
|
sleepycat |
b14f008
|
This was referenced Jun 28, 2016
cristian-sima
commented
Jul 16, 2016
|
Any update on this problem? |
|
Work on cancelable promises continues in https://github.com/domenic/cancelable-promise, and cancelation tokens in https://domenic.github.io/cancelable-promise/#sec-canceltoken-objects. This is likely the model that fetch will use. |
floatdrop
referenced
this issue
in sindresorhus/got
Aug 2, 2016
Open
Unification of promise and stream interfaces #211
ghost
commented
Aug 12, 2016
|
@jakearchibald is there any to use this currently on a browser? Maybe through some kind of polyfill? |
|
@lgdexter If request aborting is essential to your project, XHR can do it. You can abort responses in fetch using |
jeffbski
commented
Aug 18, 2016
|
@jakearchibald You mentioned that you can abort responses using response.body.cancel() but I thought that you don't get the response until the promise resolves when the request completes? Is there a way to get the response.body before the request completes so you can perform the cancel? |
WebReflection
commented
Aug 18, 2016
|
@jeffbski if I understand correctly you can consider a When you first interact with the fetch you are like at state 2, at that point you can either start processing the body as json, text, or whatever, or you can cancel it through the body that hasn't started streaming/downloading its content yet. I'm not sure if you manually export it and you |
jeffbski
commented
Aug 18, 2016
|
@WebReflection Thanks, that clears things up. I misunderstood about the response being resolved, I assumed it was in stage 4 already. If it is indeed at stage 2 then that makes sense that we still have time to abort/cancel. |
jeffbski
commented
Aug 18, 2016
|
@WebReflection It appears from the Fetch API that when the promise resolves that we must have already received the headers back since they are just a property off of the response. Thus it is fairly late in the request cycle (maybe stage 3?), you might be able to cancel and save the streaming of a large payload, but the processing time has already occurred. So I guess response.body.cancel() wouldn't save much time unless you have large payload to transfer. I did a few adhoc tests and that seemed to match this conclusion. I guess RxJS ajax is the better option for cancellation at the moment, it aborts the xhr on unsubscribe. |
WebReflection
commented
Aug 18, 2016
•
|
@jeffbski you basically discovered why this thread exists :D in XHR the During readyState 3 you can abort but not before, like you've discovered, hence the need to be able to abort, specially because that initial bit on mobile and not so great connection, is what makes the web not ideal, and in most quick-fetch cases, aborting instead of keep asking ignoring previous resolved requests, if ever, would be a great thing to do (or basically what everyone would like to do but Promise-land makes it impossible so far) |
This was referenced Aug 22, 2016
Noiwex
commented
Oct 9, 2016
|
So, guys, you seem to have some pretty discussion here, but when we're going to see a solution? |
ghost
commented
Oct 9, 2016
•
|
@Noiwex I switched from fetch to superagent. Suggest you do something similar until the fetch API is finalized |
Noiwex
commented
Oct 10, 2016
|
@lgdexter Yeah, my friend suggested me to use superagent instead too. |
jeffbski
commented
Oct 10, 2016
|
If you are only using in the browser, then can use rxjs ajax which supports cancellation. Also in addition to superagent, axios just got support for cancellable promises based on the stage 1 proposal. mzabriskie/axios#452 I hasn't been deployed in a release yet, but should be in the next one since it is in master branch. |
andrew-aladev
commented
Oct 11, 2016
•
|
This is a known limitation of js. I hate this limitation.
It is not possible. Why? Because js is not designed properly. You couldn't manage CPU scheduler. You couldn't force CPU scheduler to store full stack trace for each point. You couldn't provide a custom CPU scheduler. What is a solution?
You can see such bool guards in any javascript project. Javascript is just a toy. You shouldn't use it for any reliable solutions. |
WebReflection
commented
Oct 11, 2016
•
|
The solution is to better use timers and address them, same as you would address listeners or you won't be able to remove them later on. The concept of an ID, nothing new to learn. function Fit() {
// since you define at runtime functions
function ololo() {
console.log('ololo');
return setTimeout(ololo, 1000);
}
// you can define at runtime methods too
this.kill = function () {
clearTimeout(interval);
};
// and invoke .kill() whenever you want
var interval = ololo();
}
var fit = new Fit();
setTimeout(fit.kill, 500);JS has been historically considered a toy probably because many developers stopped learning it, convinced they mastered it already, after reading just one book. Moreover, the unaddressed timer issue has absolutely nothing to do with this problem, which is the same in many other programming languages where promises cannot be canceled. A timer, like a listener, can always be removed, if you keep the reference somehow. Best Regards |
andrew-aladev
commented
Oct 11, 2016
•
|
@WebReflection, This is not just about Could you reference and properly guard any timer, any interval, any immediate, remove every event listeners in any external library and in all it's dependencies and in all native code? No, you couldn't. So your process will dive into dead scopes forever. You could remove Javascript have to obtain a schedule management api. It's thread model is not completed. Otherwise any js developer will create |
benjamingr
commented
Oct 11, 2016
|
I think we should lock this thread. It's attracting a lot of low-level comments and discussion of how fetch should be aborted happened already and as far as I know we'll have:
Which will work. The TC has discussed cancellation with fetch as an example and @domenic is heading that effort with several others. I recommend we lock this thread - the discussion happening here at the moment is mostly noise. |
Noiwex
commented
Nov 3, 2016
•
|
It just shows that fetch specification is really underdone. |
This was referenced Nov 29, 2016
SEAPUNK
commented
Dec 15, 2016
|
The cancellable promises proposal has been withdrawn just now -- what now? |
syndbg
commented
Dec 15, 2016
|
@SEAPUNK Any specific reason why? (I don't follow the proposal discussion) |
|
Seems like things are up in the air right now. If there's no progress by January I think we should provide a way to reject the promise and cancel the stream. We have tokens or a request method as options. |
stuartpb
commented
Dec 15, 2016
•
|
I like the "revealing constructor" for a "fetch controller" approach: that lets this be orthogonal to any designs intending to create a generalized structure for cancelling promises, by focusing on a fetch-specific API. This "Fetch Controller" object could be extended down the line to add other fetch-adjusting functionality like, say, reducing the priority of a fetch (in the sense of HTTP/2, where different resource downloads can have different priorities, IIRC). |
WebReflection
commented
Dec 16, 2016
•
|
I wish we could've kept this as simple as possible, like in 20 lines of logic fix, and eventually iterate later on: // Native chainability fix
(p => {
const patch = (orig) => function () {
const p = orig.apply(this, arguments);
if (this.hasOwnProperty('cancel')) {
p.cancel = this.cancel;
}
return p;
};
p.then = patch(p.then);
p.catch = patch(p.catch);
})(Promise.prototype);
// Cancelable Promise fix
Promise = (P => function Promise(f) {
let cancel, p = new P((res, rej) => {
cancel = (why) => rej({message: why});
f(res, rej);
});
p.cancel = cancel;
return p;
})(Promise);Above snippet would've made this possible, without exposing var p = new Promise((res, rej) => { setTimeout(res, 1000, 25); });
// later on ...
p.cancel('because');I'd like to thanks the @-unnamable-one for the effort, the patience, and the competence he put, even if he'll never read this message: thank you, it's a pity developers are often and paradoxically incapable of opening their mind, instead of closing themselves in small rooms full of dogmas. |
L-Jovi
referenced
this issue
in redux-saga/redux-saga
Dec 16, 2016
Closed
how to appropriately cancel es6 fetch with saga cancel operator #701
|
@stuartpb that's another option, yeah. The overall question is should the method of aborting a request also abort the response, or are they two separate things. If they're separate, there should be some way to join them. |
stuartpb
commented
Dec 16, 2016
|
I don't see any reason a FetchController shouldn't control both, and, furthermore, allow introspection into the relationship between the two (ie. a method to query the state of the fetch, like XHR's readyState, check the progress of a response, stuff like that). |
|
@stuartpb this could help with upload progress too. Interesting. |
pimterry
referenced
this issue
in resin-io-modules/resin-register-device
Dec 16, 2016
Closed
Migrate from `requests` to `fetch` #27
kevart
commented
Jan 4, 2017
|
Really unsatisfying this whole thread. So much information and no outcome .. |
stuartpb
commented
Jan 4, 2017
|
@jakearchibald What can I do if I want to help the spec move forward with the "revealing fetch controller constructor" design (as there appears to have been "no progress by January")? |
jan-ivar
commented
Jan 4, 2017
|
I think this is all we need (requires Chrome or Firefox Developer Edition at the moment). |
benjamingr
commented
Jan 4, 2017
|
@jan-ivar can you please link to the spec of that :)? |
jan-ivar
commented
Jan 4, 2017
|
@benjamingr It's based on this discussion. |
|
Closing in favour of #447 as this thread's getting a little long, and contains a lot of discussion of cancelable promises that are unfortunately no longer relevant. |

annevk commentedMar 26, 2015
•
edited by domenic
Goal
Provide developers with a method to abort something initiated with
fetch()in a way that is not overly complicated.Previous discussion
Viable solutions
We have two contenders. Either
fetch()returns an object that is more than a promise going forward orfetch()is passed something, either an object or a callback that gets handed an object.A promise-subclass
In order to not clash with cancelable promises (if they ever materialize) we should pick a somewhat unique method name for abortion. I think
terminate()would fit that bill.Note: the Twitter-sphere seemed somewhat confused about the capabilities of this method. It would most certainly terminate any ongoing stream activity as well. It's not limited to the "lifetime" of the promise.
A controller
The limited discussion on es-discuss https://esdiscuss.org/topic/cancelable-promises seemed to favor a controller. There are two flavors that keep coming back. Upfront construction:
Revealing constructor pattern:
Open issues