Provide developers with a method to abort something initiated with fetch() in a way that is not overly complicated.
We have two contenders. Either fetch() returns an object that is more than a promise going forward or fetch() is passed something, either an object or a callback that gets handed an object.
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.
var f = fetch(url)
f.terminate()
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.
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:
var c = new FetchController
fetch(url, {controller: c})
c.abort()
Revealing constructor pattern:
fetch(url, {controller: c => c.abort()})
Thanks for the effort folks, I'm chiming in to follow up and with a couple of questions:
abort and cancel and you think that terminate would pay the naming bill. Wouldn't be wise to use similar XHR intent developers already know instead of introducing terminate for the fetch and abort for the controller?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 terminate() to avoid conflicting with cancelable-promises which might want to use either cancel() and/or abort() (as mentioned btw).
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.
I'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.
If 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)
Fetch would return a CancellablePromise that would terminate the request onCancel. The stream reading methods response.text() etc would return their own CancellablePromise that would terminate the stream.
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, fetchPromise.abort() does nothing as the promise has already settled. The correct way to write this would be:
var jsonPromise = fetch(url).then(r => r.json());
Now jsonPromise.abort() would cancel either the request, or the response, whichever is in progress.
Since calling abort might not abort the fetch (request), I don't think the method should be called abort (or cancel, as it is in some other specificatins), but rather ignore, since that is all it can guarantee to do. For example,
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 abort on it might not abort a promise early in the chain, but calling ignore will always ignore the result. For example:
//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()
Since calling abort might not abort the fetch (request)
If you call it on the promise returned by fetch() it will abort the request, but it won't abort the response. Unless of course the request is complete.
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, textPromise.abort() cancels the reading of the stream, but wouldn't abort the fetch because there are other uncancelled children. If for some reason the json completed earlier, the raw response would be buffered in memory to allow the other read. Aborting the text read would free up this buffer.
I don't think "ignore" is a great name for something that has these kind of consequences. Maybe there's a better name than abort though, I'm more interested in the behavior than the name, I just picked abort because of XHR, maybe cancel is a better fit.
@jakearchibald, in your proposal, it looks like p.then() increments the refcount, but Promise.resolve(p) doesn't? And that promises start out with a 0 refcount, so that you don't have to also abort() the initial fetch()? This seems odd to me, although it gets around the need to expose GC.
Does any of this flow through to the async/await syntax, or do you have to manipulate the promises directly to use cancellation?
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 cancellablePromise.then() increases the refcount, as would CancellablePromise.resolve(cancellablePromise), Promise.resolve(cancellablePromise) would not.
If you use async/await you're opting into a sync-like flow, so yeah if you want the async stuff you need to use promises, or we decide that cancellation results in a rejection with an abort error.
onCancel could be passed (resolve, reject) so the promise vendor could decide what the sync equivalent should be.
I'm also worried about subsequent uses of the original promise hanging instead of rejecting.
Yeah, it should onCancel.
'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 myTransform(fetch(...)).cancel(), that intermediate, uncancelled .then() will prevent the fetch from ever aborting, right? (This would be fixed if GC contributed to cancellation, but there's a lot of resistance to making GC visible.)
On the other hand, in a CancellationToken approach like https://msdn.microsoft.com/en-us/library/dd997364%28v=vs.110%29.aspx, we'd write:
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 CancellablePromise or CancellationToken if the goal here is to get something quickly instead of waiting for the long-term plan to emerge.
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.
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.
I 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 resolve and reject:
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 abort(..) on it, and that affects my internal observation of the promise, then the promise has lost all of its trustability as an immutable value.
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.
If 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 pCancel as well.
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 promise around and it's read-only immutable and observable, and you can separately decide who gets the control capabilities. For example, I'd send only promise to some external consumer, but I might very well retain the pCancel internally for some usage.
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.
async CancelIn addition to what I've observed about how promise cancelation should be designed, I don't think we should let the cancelation of a promise mean "abort the fetch". That's back-pressure, and there are other more appropriate ways to model that than promise cancelation.
In fact, it seems to me the _only_ reason you would want to do so is merely for the convenience of having the fetch API return promises. Mere convenience should be way down the priority list of viable arguments for a certain design.
I would observe that the concern of what to do with aborting fetches is quite symmetric with the concern of how/if to make an ES7 async function cancelable.
In that thread, I suggested that an async function should return an object (ahem, controller) rather than a promise itself.
To do promise chaining from an async function call in that way, it's only slightly less graceful. The same would be true for a fetch API returning a controller.
async foo() { .. }
// ..
foo().promise.then(..);
fetch(..).promise.then(..);
But if you want to access and retain/use the control capabilities for the async function (like signaling it to early return/cancel, just as generators can be), the controller object would look like:
var control = foo();
// control.return(); // or whatever we bikeshed it to be called
control.promise.then(..);
I also drew up this crappy quick draft of a diagram for a cancelable async function via this controller concept:
That's basically identical to what I'm suggesting we should do with fetch.
PS: Is it mere coincidence that canceling a fetch and canceling an async function both ended up issue number 27 in their respective repos? I think surely not! :)
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 XMLHttpRequest and streams, but I will deny that ability within this thread. Your responses have been removed for being offtopic.
@jyasskin
function myTransform(yourPromise) {
return yourPromise
.then(value => transform(value))
.then(value => transform2(value));
}
myTransform(fetch(url)).cancel();
This would:
.then(value => transform2(value)).then(value => transform(value)) because all its child promises cancelledyourPromise (which is fetch(url)) because all its child promises cancelledThis 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.
If there's a … subclass … where cancelation is a … control capability, it should be exposed in … the same way as resolve and reject
resolve and reject are internal to the promise. What we're talking about here is a way to let an observer signal disinterest in the result, and let a promise react to all observers becoming disinterested.
making a … promise reference capable of canceling the promise violates … that they are externally immutable once created
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 I vend a promise and hand … it to 3 different parties … one can unilaterally call
abort(..)on it, and that affects my internal observation of the promise
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.
@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 fetch will return some FetchObject, not Promise itself.
@jakearchibald
Before I get to the other points you've brought up (I have responses), let me focus on and clarify just this one:
signal disinterest in the result, and let a promise react to all observers becoming disinterested.
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 "child1" and "child2" still get printed? Or neither?
If merely passing parent around to various different parts of a system that separately want to observe it means that any one observer (the code that creates child1, for example) can unilaterally decide that another part of the system (the code that creates child2) is prevented from knowing about what happens with parent -- and thus just hangs around waiting in vein -- that's "action at a distance" and is a software design practice that's usually frowned upon. It makes systems harder to reason about and trust. I can follow up with illustrating a fetch(..) specific scenario I have in mind, if necessary.
Now, what happens if instead:
child1.cancel();
Does that mean that "child2" does or does not get printed? Same concerns as above.
@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. fetch() could return a subclass that has an abort method that terminates the request or in-progress response stream. The @@species of that subclass would be Promise, so calls to .then would return a regular promise, and there's no chain to worry about.
The question is whether there's a benefit in the return of cancellablePromise.then() also being abortable.
// 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();
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 parent.cancel() will trigger its internal cancellation state.
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 ;-)
@getify
Do both
"child1"and"child2"still get printed? Or neither?
Well, you get cancel is not a function since those are regular promises, but I'll assume you were meant to create a cancellable promise :smile:. In your example, assuming parent.cancel() is called prior to it resolving, neither "child1" and "child2" is logged. I can't see how either _could_ be logged… let me reword your example:
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.
If … passing parent around to … different parts of a system … means that … one observer … can … decide … another part of the system … is prevented from knowing … what happens with parent
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.
Now, what happens if instead
child1.cancel(). Does that mean … "child2" … gets printed?
"child2" _does_ get printed. The parent has a cancellable promise child count of 1 (child2) so it does not cancel. "child1" is not printed.
Is parent is cancelled before resolving, and you expected something to be logged, what would the value be?
The problem here is differing perspective and actor. I get the desire to want to cancel the parent and thus have that filter down to all observers not getting resolved (and perhaps getting instead notified of cancelation). I get, and want, cancelable fetch(..). I also want cancelable async function, for the exact same reason.
What I am objecting to is the perspective that the code that creates child1 gets to unilaterally decide that and affect the code that makes child2. Only the actor that created parent should get to decide that for all observers.
In fetch(..) context, the party that fires off the initial request should of course be able to decide later, "hey, forget that request." But downstream, if only one observer of that action (among many others) says "I don't care anymore, forget it", that should only allow that observer to cancel their observation, not affect other observers.
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.
@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 CancellablePromise parent and a child = parent.then(...), then if you want to be able to create more children in the future, before you hand child out to code that might cancel it, you have to create and hold onto your own child=parent.then() until you're sure you're done creating children. When storing parent in a cache, you might just write parent.then() and throw away the result to permanently disable cancellation. Right?
The question is whether there's a benefit in the @@species also being abortable.
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 fetch, because this will cause no more questions about "is chained promise abortable or not" since .response will be just property which just returns Promise not directly related to fetch action.
@jyasskin
until you're sure you're done creating children
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));
}
@getify
What I am objecting to is … the code that creates child1 gets to … affect the code that makes child2.
But it can't. child1 _and_ child2 would have to cancel for parent to know about it and cancel the underlying fetch.
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?
Sorry, reference should have been complexity. I blame my phone keyboard.
@jakearchibald
Requoting myself:
the code that creates child1 gets to unilaterally decide...
I'm not suggesting:
child1 = parent.then(..);
child1.cancel();
I am suggesting:
child1 = parent.then(..);
parent.cancel();
By having the reference parent to observe, I also have the same reference parent that lets me cancel it for everyone else.
@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 Promise.
I'm not too worried about this. You can have multiple listeners to DOM elements that can also mutate then. World keep turnin'.
@martinthomson
What is wrong with adding a simpler hook, solely for fetch use?
If there's no agreement on chainable cancellation, we could go with a promise subclass with an .abort that rejects the fetch if it's in progress & terminates the stream. This promise would have a @@species of Promise so there's no cancellation chaining.
Because this terminates the stream, it may be impossible for the reader (eg .text()) to know something went wrong, and may resolve with partial content - basically the same that would happen if the connection is abruptly terminated (see my research on this https://github.com/slightlyoff/ServiceWorker/issues/362#issuecomment-49011736). This isn't an issue with the CancellablePromise, as the explict cancellation can be captured.
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 .then returned a standard promise.
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.
@jakearchibald
that's bad form of the child1 code
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.
they can cast to
Promise
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 p_safe wouldn't have a cancel(..). But then, that two-step is essentially the same in spirit as:
var controller = fetch(..), p = controller.promise;
// later
child1 = p.then(..)
The problem with having "controller" be a CancelablePromise and expecting people to understand this nuance and pre-cast with Promise.resolve(..) is that most won't understand, and will just pass the cancelable-promise around, until they get bitten, and it won't be entirely clear why.
I'd call that pit-of-failure design.
@getify
I had a feeling this suggestion was coming soon
You might have gotten that feeling because I'd suggested it twice in this thread already.
@jakearchibald
Because this terminates the stream, it may be impossible for the reader (eg .text()) to know something went wrong
Should it be the same as:
fetch(...).then(function(response) {
setTimeout(function() {
response.body.close();
}, 100);
return response.text();
});
I believe .cancel() on promise should behave same as response.body.close() and promise returned by response.text() should receive an rejection (since it was explicitly abrupted).
@NekR
Should it be the same as:
No, your call to cancel the stream would fail as .text() has a lock on the stream. Cancellation is not observable in streams as far as I know. Of course, if the bytes received is < content-length, the reader could choose to reject, but not all responses have content-length.
@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 fetch(url).then(r => r.json()) isn't abortable once the stream starts being read into json.
No, your call to cancel the stream would fail as .text() has a lock on the stream. Cancellation is not observable in streams as far as I know. Of course, if the bytes received is < content-length, the reader could choose to reject, but not all responses have content-length.
That is weirdest behavior I ever saw -- you cannot cancel because you are doing request.
Well, _you_ aren't, .json() is.
@jakearchibald,
That would mean fetch(url).then(r => r.json()) isn't abortable once the stream starts being read into json.
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.
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 innocuousActivity() is cancellable. But the code that calls it doesn't expect it to be cancelled. Maybe the ability to cancel this operation wasn't even added until a recent update. But a failure there leaves means that the activity is run multiple times if ever it were cancelled. That might be OK, but you don't know if that is true; certainly steps were taken to ensure that it wasn't called twice.
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.
@martinthomson doSomethingInnocuous() already has a bug if innocuousActivity() ever rejects. I (believe I) differ from @jakearchibald in thinking that cancellation should always result in either a resolved or rejected CancellablePromise, rather than adding a third state, and if cancellation results in a rejection, then doSomethingInnocuous()'s rejection handler will get to do whatever it did in a pre-cancellation world.
@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 .catch() handlers all down the chain, but I don't see that as materially different from defining a reallyReject and matching .reallyCatch() functions.
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 CancellablePromise when the anchor promise (the one that can actually be cancelled, not its dependents) resolves? Does the .abort() function on it just fail with a TooLateError?
@martinthomson
That would mean fetch(url).then(r => r.json()) isn't abortable once the stream starts being read into json.
That's OK. At that point, cancelling the stream should suffice. That can be mapped back into a cancellation of the response.
Readable[Byte]Stream.cancel works only when the stream is not locked. As json() locks it we cannot abort fetch through stream's methods.
@yukatahirano, I could live with that. .json() is a function you call to get the entire payload anyway, so having that prevent an abort isn't too surprising. If that turns out to be problematic, add an abort to the thing that it returns too.
@jyasskin @martinthomson
differ from @jakearchibald in thinking that cancellation should always result in either a resolved or rejected CancellablePromise
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();
});
@yukatahirano, I could live with that. .json() is a function you call to get the entire payload anyway, so having that prevent an abort isn't too surprising. If that turns out to be problematic, add an abort to the thing that it returns too.
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 fetch. And you are saying what fetch design is not ugly. Okay.
Sorry for jumping back into the discussion so much later.
What we're talking about here is a way to let an observer signal disinterest in the result, and let a promise react to all observers becoming disinterested.
This is exactly why I think it should be called an IgnorablePromise and should have an ignore method, rather than an abort or cancel method. Note that I'm interested in this on a general level, not just for fetch but for other promise methods too. As long as you want to chain the promises together there is no way to guarantee that the initial action will be aborted. Instead of suggesting (through the name of the method) that all started actions will be aborted, I believe that ignore should be used to indicate that the result will not be needed. If the code (through refcounting) can realize that some work is not needed and can indeed be aborted, then that's an added bonus. But the user of the API shouldn't call the cancel/abort method expecting the work to be cancelled, since there is no way to guarantee that.
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
@jakearchibald It's up to the producer that receives a cancellation to call reject with a clear argument, but we should add a CancelledError to http://heycam.github.io/webidl/#idl-DOMException-error-names to give folks an easy thing to return. This _has_ to be up to the producer because it might not receive the cancellation in time to actually cancel, or it might have failed for a different reason and be cleaning up when the cancellation comes in.
@jyasskin, I agree precisely regarding the error type. Using instanceof is perfectly good.
As for upstream cancellation, I'd like to expand on the virtues of the .NET CancellationToken design. While I'll concede that the need for a cooperative cancellation process in a multithreaded system is somewhat diminished in the JS context, the benefits for transparency here are quite significant. In fact, this is considered the primary benefit in the .NET design: https://msdn.microsoft.com/en-us/library/dd997364(v=vs.110).aspx
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 fetch() is wrapped.
@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.
@mariusGundersen
This is exactly why I think it should be called an IgnorablePromise and should have an ignore method, rather than an abort or cancel method
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.
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.
Yep, if you want to retain cancellation but vend a child promise, it's simply cancellablePromise.then(). If you don't want cancellation it's Promise.resolve(cancellablePromise).
@martinthomson
That alone is surprising, since the second call fails. The actual results can be hard to predict if the call to fetch() is wrapped.
When calling .catch() you need to be aware of the chain, so you know which bits you're potentially catching from. I'd argue that the same level of awareness is needed for .abort().
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 canceled state.
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)
"been cancelled"
"post-cancel resolve"
"done2 undefined"
It feels weird to me that p2.cancel() would result in the original p1 promise cancelling, affecting other children that didn't cancel and were unable to observe that cancellation. I can't really work out why neither 'done1 ' + val or 'error1' is logged, but 'done2 ' + val is logged, and has resolved with undefined.
cancel actually cancels up to any promise that hasn't finished yet.
The whole point/ease of Promises is the .then chain/simplicity so the moment you cancel you want to be sure nothing happening before will reach your promise.
Cancel in my example is the "reset" point and from then on you can use .then if you want but just as already resolved Promise.
The main misunderstanding I see in your code is that you used ifCanceled as if it's an onCanceled or an onRejected but actually the cancelability should be provided as callback.
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 reject after you have invoked resolve, that will not make your promise rejected.
Accordingly, if you canceled, you canceled.
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 .cancel() method.
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 .cancel method internally too, and the reason all promises before the current one, including the current one, will be silently resolved.
No other value than undefined came to my mind as default resolution for cancelable promises.
Not sure I've explain myself properly, feel free to ask more or tell me what you think is wrong.
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)
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 ifCanceled mechanism.
@WebReflection
The main misunderstanding I see in your code is that you used ifCanceled as if it's an onCanceled or an onRejected but actually the cancelability should be provided as callback
I understood. It's the same as the onCancel in my proposal, except you've put the callback in a much better place. I just wanted to see the order of events even if resolve was called after cancel.
Again, the key here is backward compatible, and the third argument is not the one that resolves or reject
Yeah, I'm increasingly convinced that .then(… onCancelled) is a bad idea due to async/await, hanging is bad for the same reason. I don't think resolving with undefined is useful, as that's likely to break code expecting a particular value. Maybe rejecting with undefined is better (and have that hit each promise down the chain)?
I still like the ref-counting though, as a way to ensure a child cannot break an independent branch of the promise chain.
as that's likely to break code expecting a particular value.
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.
I still believe the callback to define how to cancel should be private
Agreed
Anyway, I'd rather get consensus on the feature design before bikeshedding naming.
Sure, I won't bring up naming again.
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.
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 undefined
The same applies to rejecting with undefined:
fetch(url)
.cancel()
.catch(logErrorToSomewhere);
This will log a lot of undefined to a service that is interested in errors on the frontend. With a lot of canceling (for example a search field that does autocomplete) there will be a lot of noise in the catch. To work around this you would need to check for undefined in the catch handlers, which means you need an if check in (potentially) all your catch handlers.
If you cancel you don't expect anything because you canceled. Going through reject what are you going to expect or why would even set at all that .then ?
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 .cancel(why) was working as resolution for the promise, so that you can eventually cancel with a reason that will be passed to the next .then that is not in the canceled chain, but in the waiting one. Still not sure how much sense it makes.
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 updateAutocompleteListWithResult to be called with anything.
@WebReflection
However in my initial design .cancel(why) was working as resolution for the promise
Well, because of the scoping of your cancel function (which looks weird but is great in practice), you have easy access to resolve and reject. Maybe, after the cancel callback is called, reject(undefined) is called automatically. That means the cancel callback _could_ call resolve/reject itself. That means the creator of the promise could resolve with a partial value if that makes sense.
My code will not invoke that indeed if canceled. My code silently resolve,
without invoking. Your example will work without problems.
On Mar 30, 2015 2:43 PM, "Marius Gundersen" [email protected]
wrote:
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 updateAutocompleteListWithResult to be
called with anything.—
Reply to this email directly or view it on GitHub
https://github.com/whatwg/fetch/issues/27#issuecomment-87663826.
It does make sense to me but I've created something I had to explain
already few times because unknown as pattern. I think this is inevitable
though, we need to re-think promises in order to find a successful cancel
pattern, IMO, cause what we have now does not simply work for that :-)
On Mar 30, 2015 2:47 PM, "Jake Archibald" [email protected] wrote:
@WebReflection https://github.com/WebReflection
However in my initial design .cancel(why) was working as resolution for
the promiseWell, because of the scoping of your cancel function (which looks weird
but is great in practice), you have easy access to resolve and reject.
Maybe, after the cancel callback is called, reject(undefined) is called
automatically. That means the cancel callback _could_ call resolve/reject
itself. That means the creator of the promise could resolve with a partial
value if that makes sense.—
Reply to this email directly or view it on GitHub
https://github.com/whatwg/fetch/issues/27#issuecomment-87664371.
My code will not invoke that indeed if canceled. My code silently resolve, without invoking. Your example will work without problems.
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 then or catch. If you want to send a reason, then you could do it as @jakearchibald described:
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.
I think you keep misunderstanding my proposal and code. isCancelled in your example makes no sense, it's exposing the ability to reject instead which is against Promise principle.
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 f.cancel({}) so that the following then will receive an empty object (simulating in this case an empty JSON response) avoiding any sort of surprise.
While this thread is about fetch(..) and not explicitly about the async function functionality, they are extremely symmetric and I think solutions should make sense for both.
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 fetch(..)' API, I think we'll end up at a solution that only works for fetch(..) and is inconsistent/incoherent compared to how these problems are solved elsewhere.
What's being currently discussed would mean that somehow an async function would have to be able to declare what its ifCanceled cleanup code should be in some way... I'm failing to see how that would make any sense:
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 async function, I don't see any way that you could specify this ifCanceled callback to its creation, without some sort of awkward and composition-killing hijacking of its first parameter (as someone on another thread seems to have suggested, of sorts).
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 (ifCanceled returns such method) or externally so the first in charge of canceling will be also eventually in charge of passing an arbitrary resolution for such cancellation.
@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.
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 .cancel ability ?
If you dont' want any of them why not passing a cancelable promise inside a promise so that no cancelability will be exposed ?
The await problem is also half considered ... where is the catch in your example?
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 .cancel({text:''})
I also believe if await should do exaclty what Promises do, then we have a redundant pattern impostor
I don't even care much about fetch itself in terms of solution
Right, but I think you missed my point, which is that fetch(..) and others have an explicit promise creation notion, so it's easier to see how the ifCanceled could be provided to the promise returned from fetch(..), but there are other mechanisms like async function which implicitly create promises, and offer no such mechanism.
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.
providing a canceling mechanism is the only way to go
If you're talking about canceling fetch(..) and async function and other promise-creating tasks, I agree. If you're talking about canceling the promise directly, rather than indirectly by virtue of the task being canceled, I don't agree. I don't think it follows at all that promises have to be cancelable to achieve the clearly agreed ideals that fetch(..) should be cancelable.
if you always need boths whhy not just passing a promise with .cancel ability ?
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).
where is the catch in your example?
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 catch clause here in no way shape or form could be invoked by the .cancel(..) call made down on the promise chain, unless you're suggesting conflating cancel with throw? The catch in this example is triggered if there's an error during ajax(..), not from what happens with the outside promise chain.
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
On Mar 30, 2015, at 4:56 PM, Kyle Simpson [email protected] wrote:
I don't even care much about fetch itself in terms of solution
Right, but I think you missed my point, which is that fetch(..) and others have an explicit promise creation notion, so it's easier to see how the ifCanceled could be provided to the promise returned from fetch(..), but there are other mechanisms like async function which implicitly create promises, and offer no such mechanism.
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.
providing a canceling mechanism is the only way to go
If you're talking about canceling fetch(..) and async function and other promise-creating tasks, I agree. If you're talking about canceling the promise directly, rather than indirectly by virtue of the task being canceled, I don't agree. I don't think it follows at all that promises have to be cancelable to achieve the clearly agreed ideals that fetch(..) should be cancelable.
if you always need boths whhy not just passing a promise with .cancel ability ?
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).
—
Reply to this email directly or view it on GitHub.
If all consumers stop listening for the outcome
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 fetch(..) call, and pass its promise/Task/whatever return value around to multiple observers in your system, and then something critical happens and you (the producer) decide you need to cancel the fetch(..) (abort an upload b/c user goes offline, etc), you can't. You can't reach into all those possible places where observers attached and get all of them to cancel(..) or ignore(..) or even just unset themselves for GC purposes.
Also, GC is not guaranteed to happen as soon as all refs are unset. If you want to abort a fetch(..) right now, and you unset all observers, the engine may not GC all those for seconds or more, which means the abort may very well not happen immediately.
That's precisely the point I've made many times in this thread, that you don't always need both
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 p.cancel() just for fun, you gonna have an error because I am proposing indeed backward compatibility.
The implicit cancellation suits perfectly fetch because it doesn't matter what happens in the core, behind the scene, as long as we know the object can be canceled, either via contrller or .cancel() we are good.
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 cancel as reject is a Jake idea and since I've said my code would have been simplified in that way, here I come with the example that rejects through cancel: here it is
In this case the .cancel rejects but then it breaks the whole chain so that this will stop at the first catch, although resolving everything without invoking.
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.
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 =
task.
until(formClosed). // another Task or Promise
get(value => console.log(value),
error => console.error(error));
JH
On Mar 30, 2015, at 5:21 PM, Kyle Simpson [email protected] wrote:
If all consumers stop listening for the outcome
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 of the task. If you make a fetch(..) call, and pass its promise/Task/whatever return value around to multiple observers, and then something critical happens and you (the producer) decide you need to cancel the fetch(..) (abort an upload b/c user goes offline, etc), you can't, because you can't reach into all those possible places where observers attached and get all of them to cancel(..) or ignore(..) or even just unset themselves for GC purposes.
Also, GC is not guaranteed to happen as soon as all refs are unset. If you want to abort a fetch(..) right now, and you unset all observers, the engine may not GC all those for seconds or more, which means the abort may very well not happen immediately.
—
Reply to this email directly or view it on GitHub.
Agreed that ref count would be problematic for few reasons:
.then is a pattern that favorites chainability and for some reason people love chainability, meaning it's not so easy to count down to zero within a chain, there'll always be someone at the end, unreferenced, and already attached, am I right? So how the initial fetch is supposed to abort since everyone unable to stop it can detach itself? Is is about getting rid of the initially assigned fetch? And what if I am an asshole that keeps one of the chained thenable referenced? Is everyone else screwed?In any case, this perfectly summarizes my general disappointment with Fetch
Fetch is an excellent example of an API that should not use Promises, because it involves the use of a scarce resource (ie connections).
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?
@jhusain
Can you clarify what you mean by producer?
Since fetch(..) itself is an opaque utility, it's not the "producer" I mean. It's a tool the "producer" uses.
By "producer" I really mean the code which actually makes the initial call to the fetch(..) call (or whatever API)... that may very well be the main consumer of the response. If it's the only consumer, it's no big deal.
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 heyStopListeningToThatThingISentYouEarlier(..) API.
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
On Mar 30, 2015, at 5:45 PM, Andrea Giammarchi [email protected] wrote:
Agreed that ref count would be problematic for few reasons:
GC exposed and also unpredictable in its execution
.then is a pattern that favorites chainability and for some reason people love chainability, meaning it's not so easy to count down to zero within a chain, there'll always be someone at the end, unreferenced, and already attached, am I right? So how the initial fetch is supposed to abort since everyone unable to stop it can detach itself? Is is about getting rid of the initially assigned fetch? And what if I am an asshole that keeps one of the chained thenable referenced? Is everyone else screwed?
In any case, this perfectly summarizes my general disappointment with FetchFetch is an excellent example of an API that should not use Promises, because it involves the use of a scarce resource (ie connections).
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?
—
Reply to this email directly or view it on GitHub.
Now subscription counting is simply a matter of counting the number of get calls in the system
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.
I've got the feeling that the moment we use .subscribe and .unsubcribe we're virtually back in addEventListener and removeEventListener world with just an internal counter per listener and magic cancellation provided.
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 .json read of the first fetch.
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.
this is a case where you do want the receiver of the promise to have cancellation rights.
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.
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?
why my proposal that exposes cancel-ability only if internally provided wouldn't work, exactly?
You never addressed my earlier concern that a general implicitly-creating-promise type mechanism, like async function, provides no clear way to tell the promise "make yourself cancelable" in any useful way.
fetch(..) can do so by adding some explicit parameter to its API, which it automatically funnels into its promise. But a general async function cannot, not without ugly side-effects on composability/signature design.
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 .json read of the first fetch.
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.
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
Does this answer?
No, it doesn't answer my concern. The promise returned externally by the async function is not at all related to any of the intermediate promises that are await'd internally. I don't see how my concern and what you're asserting are related. Can you clarify?
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();
It's not fully specified yet so I'd say it would work similar as generators do ... you externally invoke .next(value) there and receive the state.
If you have an async function in a world where there are cancel-able promises I don't see why foo().cancel(resolve); shouldn't implicitly invoke internal holded ajax(url).cancel(resolve) too and throw if that's not cancelable since there's nothing to cancel.
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 foo().then(...).cancel(resolve); ? Same exact thing, if you start from a cancelable Promise, all derived Promises (then/catch) are cancelable too.
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.
you externally invoke
.next(value)there
Where do you invoke this? On the return value from an async function? That's currently thought of as being a normal promise, not an iterator, so next(..) call there wouldn't make any sense.
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?
The .next was compared to .cancel as method you invoke outside. Calling
cancel on an async function means implicitly invoke cancel to the
cancelable promise hold in await. About constructing cancelable promise is
not your concern. Fetch will do what it has to do, you are not responsible
for that, the API internally knows how to cancel. If you want to make a
promise cancelable you do that, of you don't want a cancelable promise you
don't. Better?
On Mar 30, 2015 8:01 PM, "Kyle Simpson" [email protected] wrote:
you externally invoke .next(value) there
Where do you invoke this? On the return value from an async function?
That's currently thought of as being a normal promise, not an iterator, so
next(..) call there wouldn't make any sense.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?_—
Reply to this email directly or view it on GitHub
https://github.com/whatwg/fetch/issues/27#issuecomment-87774559.
Better?
Sorry, I'm still lost. Perhaps you're missing the fact that the promise which comes back from an async function call is not the promise you await inside. Totally diff promises. The external one is implicitly created by the JS engine at the time the async function starts, and is returned back completely unawares to the code inside the async function.
Calling cancel on an async function
There is no such thing. You don't call foo.cancel(..), you call foo() ... .cancel(), which means you're calling cancel(..) or next(..) or whatever on what that return value is. As design stands, that value is a promise.
We're obviously bogging down this thread and way off track. If you'd like to explore async functions more, let's go elsewhere. The point that will remain here is that your proposed solution does not have a symmetry which works in that case, which is my main concern WRT this thread.
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?
fetch(...).cancel() should be able cancel request and response (no matter for now how it all will be integrated with Promises/Streams)fetch(...) promise cancelability should be chainable. It's hard to agree for now how it will be implemented, but it seems what all in this thread (and I believe all other developers will) expect such behavior.fetch(...).cancel() is chainable then fetch also should have some grouping operators for it. fetch.all(...) or CancelablePromise.all(...) does not really matter now. async/await (which I believe should work by default since all this API is around Promises).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.
But can we all agree at least of basic concept things
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:
fetch(..) should be cancelable in some way, and that cancels the request and any response "observers".fetch(..) action is canceled, the entire down-stream chain of its "observers" must be canceled/aborted/rejected/etc, not just the first level. Ideally this cancelation would be observable down-stream (either as a rejection or as a third state).async..await and any other (future) language mechanism that either explicitly or implicitly creates promises to represent the completion of a task.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).
fetch(..) should be cancelable in some way, and that cancels the request and any response "observers".
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.
If the fetch(..) action is canceled, the entire down-stream chain of its "observers" must be canceled/aborted/rejected/etc, not just the first level. Ideally this cancelation would be observable down-stream (either as a rejection or as a third state).
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.
The cancelation capability should be something that go wherever the "obvservable"-for-response can go (whether that be as separate values, composed in a single object, or combined into promises).
Cannot understand this, but please do not continue.
Should make sense, and be equally effective, with async..await and any other (future) language mechanism that either explicitly or implicitly creates promises to represent the completion of a task.
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.
The external one is implicitly created by the JS engine at the time the async function starts, and is returned back completely unawares to the code inside the async function.
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 await will receive, implicitly, the .cancel(value) invoke whenever it happens explicitly outside the async function.
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 is no such thing. You don't call foo.cancel(..), you call foo() ... .cancel(), which means you're calling cancel(..) or next(..) or whatever on what that return value is. As design stands, that value is a promise.
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.
If you'd like to explore async functions more, let's go elsewhere.
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: async and await. Are you willing to not talk about async and await as if these were long standing standard patterns? 'cause no spec is defined yet for them, regardless written books or blogposts.
We must be able to fix things before these are out, not be stuck with documentation about partially defined standards. Agreed? I hope so ...
@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.
Otherwise it's pointless being here because everything, even controller, will fail with async and await raw syntax.
@WebReflection
@NekR I'm honestly off philosophy because we have a real need and clock is ticking.
I agree with, but that discussion seems going nowhere (all things repeated again and again, including other threads) and I cannot understand why. So I just tried to summarize things with hope what it might help all to focus.
@getify quite sad statement ... we just need to better understand each other I guess ... so here the scenario I am describing against async and await:
async function fetchLike(url) {
try {
var response = await ajax(url);
return response.text;
}
catch (err) {
// ..
}
}
var asyncPromise = fetchLike("http://some.url.1");
// after this ...
// var response = await ajax(url);
// we are virtually here^
// but now we explicitly to this
asyncPromise.cancel({});
// ajax(url).cancel({});
// the engine is implicitly doing ^^^^^^^^^^^^
// since canceling, the engine is also
// executing this instead of the original function
async function fetchLike(url) {
try {
var response; return ajax(url).cancel({});
// see this ^^^^^^^^^^^^^^^^^^^^^^^^^^^^
// that's what a cancel-able await does
return response.text;
}
catch (err) {
// ..
}
}
// outside, in a better world ...
asyncPromise.then(function (value) {
// empty result, nothing to do
return value; // keep going, if needed
});
That works to me, it's like the return inside a generator, not so different at all.
Do you need to know what happens inside ajax(url) when ajax(url).cancel({}) happens? NO, because that's the whole point. If you provide a cancelable Promise, you are the ony one that can resolve or reject it internally, _so you are the only one able to cancel_.
You provide such functionality? Others can consume it. It's exactly all the same with controllers, except you pass around directly a cancel-albe Promise instead of the Promise plus its controller.
Do you want to hide this ability?
async function fetchLike(url) {
try {
var response = await Promise.resolve(ajax(url));
// check this out ^^^^^^^^^^^^^^^^^^^^^^^^^^^
return response.text;
}
catch (err) {
// ..
}
}
You know what's the result? That if you try to cancel fetchLike now, since Promise.resolve does not return a cancel-able Promise, it will throw an error, 'cause you are not allowed.
So this pattern covers every scenario discussed in here, or am I missing anything at all?
If the question is again: how do you cancel that from the outer world? ... the answer would be again: You don't! You are not responsible for cancel-ability because as the Promise creator provides resolve and reject, the Promise creator is the ony one that can provide a cancel too without exposing its mechanism.
I rather prefer some sort of agreement instead of abandonment. This is the future of the asynchronous Web, this must be done bloody right! (and I feel it's too late already)
@WebReflection, I'm starting to come around to the idea that cancellation has some sort of retroactive action. I would still (rather strongly) prefer that this not be done without cooperation from the code that is being affected by this change. That said, my objections have been addressed, at least from a technical standpoint.
I'm still totally against the idea that cancellation would manifest as success for several reasons. If you have a long chain of fetches, you have no way of knowing which you are targetting with this, so rejection is the only way to ensure that you don't cause real problems.
Honestly, I never considered anything other than rejection as an option here.
I'm a little perplexed at the notion that you might somehow know to guard against cancellation. If this is the right thing to do, then it should work uniformly and Promise.resolve() would be insufficient; though creating a new unresolved dependency might be OK, even if that dependency was a noop.
the guard against cancellation is simply a non-cancel-able Promise because I think .cancel() should probably not be invokable by default. Same as then is the method key to figure out a Promise from a generic object, cancel is in CancelablePromise-land the method key to know if a Promise is cancelable.
Since cancelable promises resolve their state regardless, these can be wrapped without problems through regular Promises, whenever we reach them before others and we don't want to provide .cancel() method outside our little world when we pass the Promise around.
In plain Vanilla JS, and using your rejection approach, this is technically what I mean:
// simulating my second proposal manually
var cancel, cancelable = new Promise(function (res, rej) {
// le async resolution
var interval = setTimeout(res, 1000, 'OK');
cancel = function (why) {
clearTimeout(interval);
rej(why);
};
});
// making it cancel-able out there ...
cancelable.cancel = cancel;
// now this would be possible ...
// cancelable.cancel('no-more');
// but now I intercept this object
// and I'd like to not make it cancel-able anymore
var nope = new Promise(function (res, rej) {
cancelable.then(res, rej);
});
// passing back `nope` instead
// so that whoever tries the following
nope.cancel();
// will fail ... but
// instead of that ...
// this would have worked
cancelable.cancel('because');
Now you have a cancelable Promise inside a regular nope Promise that cannot be canceled.
This is option number 2 in my opinion, but I agree it simplifies the whole circus.
It's also probably more pragmatic since there's no canceled virtual state to resolve, and developers can react as soon as they can understand what's the cough error about.
P.S. once again, previous snippet playground is here the main difference, in terms of behavior, is that whenever you .then(...) or .catch(...) a cancel-able Promise, it MUST create a Promise that is able to cancel its parent. This preserves Promise chain-ability, propagating the cancel-able intent down the road (actually up the road) .
// simulating my second proposal manually
var cancel, cancelable = new Promise(function (res, rej) {
// le async resolution
var interval = setTimeout(res, 1000, 'OK');
cancel = function (why) {
clearTimeout(interval);
rej(why);
};
});
// making it cancel-able out there ...
cancelable.cancel = cancel;
// now the regular user-land behavior
cancelable.then(...).then(...).then(...) // and eventually .cancel();
// even at the end of that chain
@martinthomson last one to really try to avoid misunderstanding
I'm still totally against the idea that cancellation would manifest as success for several reasons.
my first proposal wan't resolving all pending promises with a value, it was ignoring all of them, resolving silently, without invoking any then in the chain up to the cancel but resolving the canceled one with an arbitrary value.
I see this is apparently sci-fi and impossible to explain even with a working playground so I start agreeing rejecting would most likely be welcomed more from the community.
Just chiming in that all of our UIs at Netflix have used a system similar to @jhusain's Task proposal for the past few years, and it's been exceptionally effective and scalable. It provides both explicit cancellation of the fetch if it's needed, as well as automatic cancellation if everyone stops listening.
The important distinction from Promise is that .then on a Task describes a data transformation without adding a listener for the result. You need to call .get, providing callbacks for result/error if you want to start listening. Promises conflate the two actions, which means it's not possible to tell when consumers no longer care about completion.
I'd like to understand if using a different API instead of a Promise it's even an option here because AFAIK ... it's not.
There's no notion of Task in WHATWG specs but there is apparently a Promise Interface
I'm not against Tasks, I just believe we should stop talking about them unless the switch from Promise is possible.
@zowers mentioned in bluebird discussion this thread, so I've summarized the current state for this one accordingly with my understanding. Let's keep it here or try to join forces and benefit all together.
@WebReflection it's entirely possible to add it as long as you add a .toPromise on it and have it play nice - that said I'm not sure it's a good idea.
Personally: I'm not a fan of conflating promises with cancellation from a conceptual point of view but I'm also in favor of a pragmatic solution. I find cancellation terrifying - it's like a ThreadAbortException oThings since it stops an ongoing operation. A promise represents the _result_ of an operation and not hte action itself and suddenly the promise has to be aware of the actual work being done and that's really bad conceptually. That said like fetch need this ability for sure.
@jhusain kris argues this point here: https://github.com/kriskowal/gtor
Also here is the similar discussion for PromiseKit (Objective-C promises) - quoting @kriskowal :
My position on cancellation has evolved. I am now convinced that cancellation is inherently impossible with the Promise abstraction because promises can multiple dependess and dependees can be introduced at any time. If any dependee cancels a promise, it would be able to interfere with future dependees. There are two ways to get around the problem. One is to introduce a separate cancellation "capability", perhaps passed as an argument. The other is to introduce a new abstraction, a perhaps thenable "Task", which in exchange for requiring that each task only have one observer (one then call, ever), can be canceled without fear of interference. Tasks would support a fork() method to create a new task, allowing another dependee to retain the task or postpone cancelation.
...
Rejection from the producer is different than rejection from the consumer. Rejection from the consumer is a POLA violation, giving consumers the ability to interfere with each other’s progress. This manifests as the "Action-at-a-distance" antipattern. Since errors can flow both upstream and downstream, they can be broadcast laterally throughout a system. The reason for keeping the resolver and promise separate is to ensure that data flows in one direction, making programs robust and composable.
Also relevant is https://github.com/kriskowal/gtor/blob/master/cancelation.md#canceling-asynchronous-tasks
@WebReflection some questions about your gist/proposal,
// sync cancellation - does what I expect
var d = Promise.all([new Promise(function(resolve, reject, c){
c(function(){
setTimeout(alert.bind(null, "Cancel"));
});
setTimeout(resolve);
})])
d.cancel();
cancel is called multiple times? Are additional calls no-ops? This is the behavior with the current gist and it sounds correct.then doesn't even know it assimilates the child promise yet - after all)..race promises which would not cancel cancellable promises.@benjamingr reading also this (about your previous comment) from here: https://github.com/kriskowal/gtor/blob/master/cancelation.md#canceling-asynchronous-tasks
That being said, the desire for promises to support cancellation is well-grounded. If a promise represents a large quantity of work, as it often does, it does not make sense to continue doing that work when there remain no consumers interested in the result. It makes even less sense if the consumer goes on to ask a similar question that will require the same resources, like a database lock or even just time on the CPU. The issue compounds further when the user is fickle and frequently changing its query, as in search-as-you-type interfaces.
But answering your quote:
My proposal also returns the cancel method internally, once defined, giving extra control/power to the Promise creator/owner in case of exposed cancel-ability.
To answer now other questions.
Promise.all and anything that has to deal with a group of Promises should expose cancel only if all Promises in the group are cancelable ... I call it shenanigans otherwise. Please note your example is wrong because you are not providing a way to cancel the timeout so your cancelable Promise does nothing except rejeting (the timer will obviously fire but if canceled the Promise cannot be resolved after being rejected).then after a reject with this proposal, there was a .then after cancel with the initial one, but that is based on the equivalent of a task fork where the previous branch is silently resolved and never executedvar t = setTimeout(res, 100 - i, i); does the same, no reason to bind, setTimeout by standard accepts extra arguments ;-) ... 2 no reason to attach p.cancel().catch(function(){}); that catch ... promises are resolved regardless (or were you trying to prevent the uncaught Promise error?)Apologies if I haven't answered everything properly ... I'm trying :-)
"One is to introduce a separate cancellation "capability", perhaps passed as an argument" is exactly what I've proposed, a third argument that, if invoked, will define how to cancel the Promise
This is not what I believe Kris means here. What Kris is talking about here is a token like approach more akin to what @getify suggested - though I only recall this from IRC and I might be mistaken.
"_ each task only have one observer (one then call, ever), can be canceled without fear of interference_" is also possible with my proposal, which gives you the ability to have a cancelable Promise but pass around a Promise that is not cancelable so that only "you" can observe and cancel it
I don't believe this is what Kris means here either (based on the link further down to kriskowal/gtor) - the issue is that like I believe @jhusain was trying to say you can get weird tree like - cases if the cancellable can be observed by multiple observers and some try to cancel it without the knowledge of the others (what kris calls "interference"). A task would not have this problem simply because it has a single observer (in which case it is cancellable) and if you convert it to a promise you can no longer cancel it.
I'm not saying I agree with Kris on the above - it's entirely possible that just pushing fetch out already with promises that are cancellable (although there might be better theoretical alternatives) is a better pragmatic choice - or the issue might not manifest itself in real code. I do think that the issue presented here should be investigated and problematic cases should be found and discussed which is why I brought Kris's post up.
I believe Promise.all and anything that has to deal with a group of Promises should expose cancel only if all Promises in the group are cancelable ... I call it shenanigans otherwise.
So now Promise.all has to be aware of cancellation and cancellable promises? Do we want to overload .all? Do we want CancellablePromise.all instead or something like that?
Please note your example is wrong because you are not providing a way to cancel the timeout so your cancelable Promise does nothing except rejeting (the timer will obviously fire but if canceled the Promise cannot be resolved after being rejected)
Yes - I didn't care about actually doing something useful in cancellation - just the API :)
correct for multiple no-ops, I simply followed what happens if you resolve or reject after having already resolved or rejected ... nothing happen (I don't like it but that's how Promises work so far). This makes life easier if you cancel more than once in a chain of cancelables
That makes sense - a second time noop'ing has advantages - so does throwing an exception. I agree that noop is more similar to existing resolve/reject semantics.
not sure I understand the question but I can say two things: 1 var t = setTimeout(res, 100 - i, i); does the same, no reason to bind, setTimeout by standard accepts extra arguments ;-) ...
Old habits die hard :)
2 no reason to attach p.cancel().catch(function(){}); that catch ...
That's there since it suppresses unhandled rejection detection, since the promises are cancelled there are now unhandled rejections in the code which the browser tooling finds up and reports to the dev tools. I was just suppressing that :)
promises are resolved regardless (or were you trying to prevent the uncaught Promise error?)
Yeah, should've read the last sentence first. What I meant though - is do you think that more constructs need to be introduced as static promise methods to the library or not in the light of cancellation?
So now Promise.all has to be aware of cancellation and cancellable promises?
if it returns a cancellable which invokes .cancel() to all Promises there's nothing we should do since it will simply throws as soon as one of these promises is not cancellable. KISS enough so no need to overload ;-)
Do we want CancellablePromise.all instead or something like that?
That's another way to go ... I personally don't think it's needed, if developers invoke cancel just for fun they'll have errors as soon as they encounter a non cancellable promise. I honestly think cancellability is a less common case and when exposed is well known to developers. Do we want a specific contructor so that we cannot again use instanceof but we need to write more? I think having a third optionally used parameter to current Promise is good enough solution.
That makes sense - a second time noop'ing has advantages - so does throwing an exception. I agree that noop is more similar to existing resolve/reject semantics.
I don't have strong opinion here, I've played consistently with current resolve and reject behavior.
is do you think that more constructs need to be introduced as static promise methods to the library or not in the light of cancellation?
I don't think this is a concern right now but I have a counter question: shouldn't .cancel() avoid the notification on console? Maybe I can set a .catch(Object) when .cancel() is invoked so if there was one, it won't show the error, if there wasn't, it will just be there as proxy for the Error
That's another way to go ... I personally don't think it's needed, if developers invoke cancel just for fun they'll have errors as soon as they encounter a non cancellable promise.
I disagree with this. Consider the following:
var p = Promise.all([
CancellablePromise.resolve("hey"), //resolves right away
fetch('/really/fast/url'), //resolves in less than 100ms
fetch('/really/slow/url'), //resolves in more than 100ms
doSomethingNonCancellable()
]).then(() => console.log('done!'));
setTimeout(() => p.cancel(), 100);
This will cancel the Promise.all, so console.log('done!') is never run. Great! But the promises inside it might have resolved or they might be pending, and cancelling them can result in different things. The first one is obviously resolved, so there is no work to cancel. The second one is also resolved, but it has done a lot of work for no reason, since the result it resolved to will never become available. Only the third one has work that can be cancelled, which will save a few CPU cycles. So what happens to the fourth one?
If the non-cancellable promise has already resolved, then it will behave exactly like the fast url fetch, where it has done a lot of work with no observable results. But if it is still pending, then ignore whatever it resolves to and let it continue to work. What is the difference between letting a non-cancellable promise work too much and ignoring its result and cancelling a cancellable-promise after it has done a lot of work and resolved?
Cancelling a promise does not guarantee that work won't be done or that power will be saved. It will only guarantee that the promise won't resolve while it is pending. This is true both for chained promises and for parallel promises.
fair enough, it seems more practical and more friendly. I just usually prefer knowing what's going on that if I invoke cancel I expect a rejection while you are assuming that if you have Promise.all([CancellablePromise.resolve("hey"), CancellablePromise.resolve("hey"), CancellablePromise.resolve("hey")]).cancel() that should silently keep going because there was nothing to cancel ... right ?
At this point we can have a .cancel() noop by default in the prototype for every Promise, even if no cancelability has been provided.
I have one last concern, that I have already privatly solved ... if a cancelable Promise returns inside a then another cancelable Promise, should the last cancel eventually cancel this one, instead of the original one that generated the chain?
// how it is now
var a = new Promise(function (s, j, c) {
var t = setTimeout(function () {
console.log('a');
s('a');
}, 1000);
c(function () {
clearTimeout(t);
console.log('a has been canceled');
});
});
var b = a.then(function (a) {
// if this is returned
// should it be cancellable ?
return new Promise(function (s, j, c) {
var t = setTimeout(function () {
console.log('b');
s('b');
}, 1000);
c(function () {
clearTimeout(t);
console.log('b has been canceled');
});
});
});
var c = b.then(function (b) {
console.log('c');
});
// after `a` and `b` won't be canceled
setTimeout(c.cancel, 1500);
// try this to cancel `a` instead
// setTimeout(c.cancel, 750);
Locally I have a slightly modified version that in case a cancelable Promise is returned, upgrade the chain so whoever cancel will cancel the last cancelable in the chain.
This seems like a better/least-surprise behavior to me. Thoughts?
should silently keep going because there was nothing to cancel ... right ?
Yes, since all the promises in the array are fulfilled the promise returned by Promise.all() will also be fulfilled, so it is too late to cancel it. Maybe the cancel() could return a pending promise:
var p = Promise.all([Promise.resolve()]);
//p.@state = "fulfilled"
var q = p.cancel();
//p.@state = "fulfilled"
//q.@state = "pending"
if a cancelable Promise returns inside a then another cancelable Promise, should the last cancel eventually cancel this one, instead of the original one that generated the chain?
The promise returned by then(func) is not the promise returned by the function func:
var innerP;
var outerP = Promise.resolve().then(r => (innerP = new Promise(r => r()));
outerP === innerP //false
When calling c = a.then(() => b), then c is a promise that is only ever waiting for one other promise to resolve, either the previous one in the chain (a) or the promise returned by the function passed to it (b). This means that calling cancel on c will only cancel the one it is waiting for. If a hasn't resolved yet, then a will be cancelled and nothing will happen to b, since it hasn't been started yet. If a has resolved, then a will not be cancelled (since it is resolved), and b will be cancelled. If b has resolved, then c has resolved, and cancelling c will be a no-op. It will be too late.
Ok so my current playground does not work and my concern was concrete. I
have better code that if a is resolved and b is not yet and c is cancelled,
b gets cancelled. Right now even if b hasn't done yet c.cancel tries to
cancel a as root of the chain. Will update the playground soon. Cheers
On Mar 31, 2015 5:10 PM, "Marius Gundersen" [email protected]
wrote:
should silently keep going because there was nothing to cancel ... right ?
Yes, since all the promises in the array are fulfilled the promise
returned by Promise.all() will also be fulfilled, so it is too late to
cancel it. Maybe the cancel() could return a pending promise:var p = Promise.all([Promise.resolve()]);//p.@state = "fulfilled" var q = p.cancel();//p.@state = "fulfilled"//q.@state = "pending"
if a cancelable Promise returns inside a then another cancelable
Promise, should the last cancel eventually cancel this one, instead of the
original one that generated the chain?The promise returned by then(func) is not the promise returned by the
function func:var innerP;var outerP = Promise.resolve().then(r => (innerP = new Promise(r => r()));
outerP === innerP //falseWhen calling c = a.then(() => b), then c is a promise that is only ever
waiting for one other promise to resolve, either the previous one in the
chain (a) or the promise returned by the function passed to it (b). This
means that calling cancel on c will only cancel the one it is waiting
for. If a hasn't resolved yet, then a will be cancelled and nothing will
happen to b, since it hasn't been started yet. If a has resolved, then a
will not be cancelled (since it is resolved), and b will be cancelled. If
b has resolved, then c has resolved, and cancelling c will be a no-op. It
will be too late.—
Reply to this email directly or view it on GitHub
https://github.com/whatwg/fetch/issues/27#issuecomment-88126830.
Having thought about it for a bit I'm not so sure about some of the technicalities anymore. For example, does Promise.resolve(42) return a pending or resolved promise? Is there any way to observe the difference? If it returns a pending promise then it could be cancelled, and it would be (effectively) forever pending.
Here is my proposal which I believe may solve cancel-ability for fetch and more. Please take a looks, there are enough text so I decided to post it in separate gist:
@mariusGundersen I've got this in console:
var p = Promise.resolve(42);
undefined
p
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 42}
So that even bringing in my latest updated playground it works as expected.
var p = Promise.resolve(42).cancel();
undefined
p
Promise {[[PromiseStatus]]: "resolved", [[PromiseValue]]: 42}
@WebReflection
shouldn't .cancel() avoid the notification on console?
I don't think it generally should since it's still a rejection conceptually. It should be avoided in that specific case where the request is cancelled by the aggregation method (giving it ownership over its error state).
@mariusGundersen
does Promise.resolve(42) return a pending or resolved promise?
Resolved, this is in the spec.
The more I think about it the more I lean towards a different abstraction for cancellation since a promise is for a value and not about an action. A promise is simply not _expressive_ enough to denote cancellation for the same reasons progression failed.
Then again the more I think about it the more I'm willing to compromise API goodness in order to deliver fetch with working promises.
What about a super dead simple approach where you can cancel a fetch returned promise but cancellation does not propagate or aggregate in any way.
p = fetch(...);
p.cancel(); // cancels the promise
p.then(Object).cancel(); // TypeError, undefined is not a function
Promise.all([p]).cancel(); // TypeError, undefined is not a function
The cost is having to keep a reference to the original promise. If you want to add it to another promise perhaps one could monkey patch the cancel on the new chained promise.
This averts all the messy bits in the cancellation proposals since there are no tree or graph cases, there is no unexpected propagation or unexpected aggregation and so on. I'm wondering where this does not work. It seems a lot more explicit and verbose though.
that's less handy/pragmatic and with latest change I've made I don't see problems, rather ease of use. However, monkey patching would be possible so that whoever holds the first reference can decide what to do. Will simplify even more my code then for this third option.
wait just a tiny second @benjamingr : what if once canceled the then() won't return a cancelable Promise anymore? Or you just don't want to deal with any sort of cancellable chain at all?
wait just a tiny second @benjamingr : what if once canceled the then() won't return a cancelable Promise anymore? Or you just don't want to deal with any sort of cancellable chain at all?
From what I can tell - not having a cancellable chain avoids all the issues raised here (and those raised by Kris) completely and solves the most common use case of just aborting a request. It does not require promise specification support either - there are no conflicts or aggregation issues either.
I'm not convinced this is the right approach. Basically what I'm looking for is to be sold on why chaining and aggregation is important enough to require dealing with semantics like reference counting (ala what Jake's API), termination rather than cancellation (ala NekR's proposal) suggested or library-like cancellation (ala your gist).
I've written the simplest cancelable behavior that reflects what you wanted. I don't have to sell anything specifically I just hope the solution here will not be an ad-hoc thing for fetch only but rather the minimum common denominator to have cancelable Promises, which is the goal, regardless how nicely and APIish it could play.
The termination idea, as it is, is incomplete and broken in userland.
I'm sorry if it sounded like I was implying motives here and I really appreciate you taking the time to do all this.
To be fair I don't have a particularly strong opinion about any of the suggested approaches yet beyond hunches, syntactic beauty and previous debates. I'll build more implementations of common use cases for cancellation with the different approaches and report back what does and doesn't work.
On Mar 31, 2015, at 20:56, Andrea Giammarchi [email protected] wrote:
I've written the simplest cancelable behavior that reflects what you wanted. I don't have to sell anything specifically I just hope the solution here will not be an ad-hoc thing for fetch only but rather the minimum common denominator to have cancelable Promises, which is the goal, regardless how nicely and APIish it could play.
The termination idea, as it is, is incomplete and broken in userland.
—
Reply to this email directly or view it on GitHub.
The termination idea, as it is, is incomplete and broken in userland.
Sure it's incomplete, this is why it's idea. Do you think it will be complete if I will simulate it in JS like you did for cancellation? As far as no one idea was accepted here it's okay to have alternatives which may work and work good.
@NekR by all means do write a short implementation and I'll try it out on simulated workloads.
On Mar 31, 2015, at 21:07, Arthur Stolyar [email protected] wrote:
The termination idea, as it is, is incomplete and broken in userland.
Sure it's incomplete, this is why it's idea. Do you think it will be complete if I will simulate it in JS like you did for cancellation? As far as no one idea was accepted here it's okay to have alternatives which may work and work good.
—
Reply to this email directly or view it on GitHub.
@benjamingr sorry but to better explain myself, and thanking regardless @NekR for his contribution, that proposal implementation, the way I understand it, is as simple as this:
Promise.prototype.end = function () {
// something impossible to implement in userland
// that will automagically terminate whatever
// execution state is inside the Promise function
// so that everything in the function will be frozen
// and threw away to grant the end of the Promise
};
Sure such sorcery might be implemented natively, but that means we missed a huge opportunity to make it right for both core/fetch/native and users defined APIs, 'cause as a user I need to drop timers/events/workers/anything asynchronous the moment that .end() is called, and unless that proposal implements mine too, providing a way to define the onEnd callback within the Promise function, there's not much users could do with such "standard".
That plus the implicit exit behavior that does not fit with anything Promises, async, or await, related.
@WebReflection
I believe you do not hear me. I am not talking what there should be magic which cancels all potential async operations inside executor. This is approximately how it should be implemented for subclass:
class AjaxPromise extends Promise {
constructor(...) {
var xhr = new XMLHttpRequest();
// ...
this[Symbol.lock] = () => {
xhr.abort();
}
}
}
var ajax = function(config) {
return new AjaxPromise(config);
};
That plus the implicit exit behavior that does not fit with anything Promises, async, or await, related.
That is exactly what promises missing. So exit behavior does not fit, but cancellation behavior fits? I really do not see here much differences between these two concepts. async/await will form fine too.
exit does not reject so await is not meaningful anymore the way it is with cancellation, right?
Anyway, if your Symbol.lock assignment provides what my third argument does then we are good. I'd rather point that your examples assumes that has to be defined at runtime (same as my .cancel()) but it's not clear what would happen if no symbol is provided what an .end() call should do in case of Promise.resolve(42).end()
exit does not reject so await is not meaningful anymore the way it is with cancellation, right?
It should work with await like this:
async function doReq() {
var req = ajax(...);
setTimeout(function() {
req.end();
// here if req was settled at this moment this will have not effect and
// console.log will be executed, but if not, then |end()| will lock |req| promise
// and all pending consumers since they are doing nothing
// after that call of |doSomething| will be GCed
}, 100);
return req;
}
async function doSomething() {
var response = await doReq();
console.log('got response', response);
}
doSomething();
but it's not clear what would happen if no symbol is provided what an .end() call should do in case of Promise.resolve(42).end()
I believe Promise (next version of promise, all in my mind of course) should have default action for Symbol.lock. In case of Promise.resolve(42).end() it should be simply GC operation for 42 and locking that promise (all methods becomes no-op, so .then() will return other locked promise, or some sort of stub).
Anyway, I understand what my proposal is not ideal since it's made in a few hours and I have no more free time for it yet. But I am happy to answer question if it might help.
One issue with canceling an entire chain is that it could leak memory. Currently a pending promise can be garbage collected if nothing in referencing it. In the chain c = a.then(() => b), a can be garbage collected because neither b nor c hold a reference to it (a holds a reference to c).
With cancellation there needs to be a reference in both directions, so c needs to have a reference to a so it can cancel a.
This isn't a very big issue though, since c can drop it's reference to a the moment it is cancelled or resolved (by a), thereby breaking the reference chain. This means that there will only be a memory leak for forever pending promises:
var neverResolving = new Promise(r => 'I never resolve');
var leaker = neverResolving.then(a => 'I hold on to neverResolving');
neverResolving = null;
//if leaker is a normal promise, neverResolving will now be garbage collected.
//if leaker is a cancellable promise, neverResolving will not be garbage collected until leaker is!
Since this is only an issue with promises that never resolve, I don't think it is much to worry about.
Agreed it'd be handy to have cancelable chain but I also agree for
simplicity sake my latest playground with no chain at all might work as
well, providing the ability to eventually monkey patch on top without
somehow encouraging cancelability as default pattern. It's still unclear,
since apparently non cancelable promises should throw, what would be the
behavior of Promise.all().cancel() ...maybe that's also not needed in
core...
On Apr 1, 2015 3:56 PM, "Marius Gundersen" [email protected] wrote:
One issue with canceling an entire chain is that it could leak memory.
Currently a pending promise can be garbage collected if nothing in
referencing it. In the chain c = a.then(() => b), a can be garbage
collected because neither b nor c hold a reference to it (a holds a
reference to c).With cancellation there needs to be a reference in both directions, so c
needs to have a reference to a so it can cancel a.This isn't a very big issue though, since c can drop it's reference to a
the moment it is cancelled or resolved (by a), thereby breaking the
reference chain. This means that there will only be a memory leak for
forever pending promises:var neverResolving = new Promise(r => 'I never resolve');var leaker = neverResolving.then(a => 'I hold on to neverResolving');
neverResolving = null;//if leaker is a normal promise, neverResolving will now be garbage collected.//if leaker is a cancellable promise, neverResolving will not be garbage collected until leaker is!Since this is only an issue with promises that never resolve, I don't
think it is much to worry about.—
Reply to this email directly or view it on GitHub
https://github.com/whatwg/fetch/issues/27#issuecomment-88489237.
Ok, so I've played with @WebReflection 's proposal for a while now (forever pending) and its semantics are actually pretty nice in demo code I've written. I still find it problematic from a conceptual point of view but ignoring the value (signalling we are disinterested in it) which lets it clean up worked well for what I tried in practice (autocomplete like, aborting requests etc).
While I'm still uneasy with the conceptual level, from a pragmatic PoV I'm in favour of it - the semantics here are also pretty good minus the uncatchability.
I wrote 3 proposals ... which one you actually think would be the best candidate? Not sure I understood that: the silently resolved one, the chainable cancel that rejects, or the reject only?
(also none of them should be a forever pending)
About @petkaantonov semantics, I personally find it very inconvenient to need to export cancelability to the external scope somehow in order to pass it back to the callback passed to .cancellable() ... it feels like an unnecessary overhead and once again the cancelability comes from where resolve and reject are passed in first place ... I feel like my optional third argument is easier and more practical than that.
@WebReflection .cancellable is old api, new api doesnt have it
I've implemented a Task library to demonstrate a more elegant way of
handling cancellation than rejecting Promise chains. Tasks have nearly the
same API as a Promise, but Tasks must be run explicitly. When you run a
Task you get a subscription option you can use to stop observing the Task.
When a Task detects that it has no more observers, it can optionally cancel
any pending actions scheduled in order to resolve it.
A Promise represents the eventual value of an asynchronous unit of work,
but not the unit of work itself. A Task is an abstraction over both the
unit of work and its eventual value. That's why a Task is cancellable, and
a Promise is not.
Here's an example of a program which waits for a user to click a button and
then issues a network request to retrieve a stock quote. If the button is
clicked again while the network request is still pending, the current
network request is aborted and a new one is issued.
var outgoingRequest;
function waitForQuote() {
// if there's a request in-flight, stop observing the result
if (outgoingRequest) {
outgoingRequest.dispose();
outgoingRequest = undefined;
}
outgoingRequest =
Task.
// wait for next button click...
nextEvent(getQuoteButton, 'click').
// return a task with the stock quote and auto-unwrap result just like Promise's then
when(function() {
return getQuote('NFLX');
}).
// run and await the result
run(function(val) {
// display the price
priceTextBox.value = val;
outgoingRequest = undefined;
waitForQuote();
});
}
waitForQuote();
Check out the repo here (
https://github.com/jhusain/task-lib/blob/master/README.md). In there I
demonstrate many more usages of Task. Hopefully after reading it will
become clearer why I think choosing a more appropriate abstraction for
async tasks is preferable to bolting cancellation semantics on to a
Promise.
You can also play with a live (but stubbed) example here:
http://requirebin.com/?gist=533ad1e9d573ea7e9c5e
Later this week I'll fork the fetch polyfill to demonstrate how elegantly
Task could replace Promises in this API.
On Thu, Apr 2, 2015 at 12:22 PM, Petka Antonov [email protected]
wrote:
@WebReflection https://github.com/WebReflection .cancellable is old
api, new api doesnt have it—
Reply to this email directly or view it on GitHub
https://github.com/whatwg/fetch/issues/27#issuecomment-89015617.
@jhusain I think nobody here disagree Tasks are a better fit for anything network related (or anything that should be cancelable) but _Fetch API_ already shipped and it has been documented as a _Promise_ and there's no notion in WHATWG specifications about Tasks.
Accordingly, which would be the less evil solution _based on Promise_ in your opinion?
Every proposal that tries to expose cancellation semantics along with promises has an analog in synchronous programming.
Reject the promise on cancellation? You're just using error handling paths for normal program control flow. Is it an error that a user closes a form while a network request is in flight? Is it an error that a user types another key and auto complete box while a network request is in flight for the previous search? These are not exceptional cases, but common and expected situations.
What if we resolve with a special return code? (undefined? Null? 0?) Now we are back to the days of C before exceptions where every API used a special sentinel return value for error conditions, and your algorithm was lost in endless branch conditions.
These approaches were abandoned for good reason. Just because we are building asynchronous APIs does not mean every well-established principle of good API design is up for debate.
Let's say we can stomach either of these approaches? What will we have accomplished? Now any module may be able to cancel a promise that any number of other modules may depend on. The ability for anyone party to cancel a fetch will turn every fetch Promise into shared, mutable state. This a foot gun, and we should not be promoting this anti-pattern when better alternatives are available.
I have a concrete proposal: if you need cancellation, don't use fetch.
Fetch is not the world's first less-than-perfect standard. Well intentioned standard bodies can certainly make mistakes. I know, I'm on one that has made a few mistakes despite the best efforts of everyone involved to avoid them. If standardized APIs don't meet developer's use-cases, they go unused and are gradually deprecated. It is not ideal, but it is not fatal either.
When standards do not meet their needs, developers use libraries instead. If the libraries are successful enough, standards bodies will move to make these successful libraries part of the web platform. That is how the extensible web is supposed to work.
I propose that we create a fetch-like API that uses Tasks ("fetchTask"). Ideally the API should be designed so that if developers run up against the limitations of fetch, they can easily modify their code to use fetchTask.
If cancellation is a rare edge case (which it may well be), the library will not pick up a lot of steam. However if developers hit this limitation regularly, the library will gradually become more popular. This will hopefully sway those who make standards by demonstrating to them the demand for this kind of API. That is a great path to standardization, and a better idea than trying to bolt cancellation semantics onto a promise-returning API.
JH
On Apr 3, 2015, at 1:10 PM, Arthur Stolyar [email protected] wrote:
@WebReflection all except creators of spec. because they still think it was good idea to make whole fetch design around promises. Anyway, I should stop here because this comment is candidate for removal.
—
Reply to this email directly or view it on GitHub.
this is where I usually ignore all the philosophy ...
Ideally the API should be designed so that if developers run up against the limitations of fetch, they can easily modify their code to use fetchTask.
so that all pending Promises will have the "OMG mutable" state because the initial request needs to be cancelable.
If cancellation is a rare edge case ...
It's not. Everything you POST might need to be canceled and once again we all agree fetch was unfortunate choice as XHR replacement but you are suggesting to have duplicated effort and standards because of a very simple and pragmatic and needed .cancel() ... so I usually drop the line where all these discussions become simply pragmatically pointless and against the interest of all Web Developers.
Yeah, you want to cancel that request because that's what we've been doing since about ever with XHR ... having XHR and a broken Fetch replacement for XHR so that we can have a fetchTask as other standard that's practically identical to Fetch except the cancelability would be a benefit for nobody due time it takes to ship new APIs and all the confusion around this Fetch potentials.
I rather deprecate Fetch if FetchTask is the answer, or follow pragmatic MS approach and bring Streams to XHR so people that don't need to control network requests can use Fetch and all others can live happily ever after with XHR.
It's just sad we are apparently not moving anywhere here ... I start even dropping interest on this matter.
Also last thought on this: by specifications, the User Agent is capable of dropping the request internally triggering a Fatal error so all this immutable Promise is a lie because it's just virtually effective in User Land but in core the state is mutable because that's needed
So how do we move on here, since deprecating fetch thread has been closed, and Tasks are not even close to be a specification standard?
Mutable state is perfectly okay, as long as the mutation is not observable. If a task is canceled once all consumers stop awaiting its result, it's like a tree falling in the forest when no one is around to hear it. If I can cancel your promise, that's mutation that you can observe. That's a recipe for disaster.
I understand your frustration Andrea. Everybody wants standards to be well-thought out the first time around. It does seem like we are sort of agreeing though. Ideally fetch would not exist, and if it truly does not meet the needs of developers, it will eventually fade away. Out of all the options we have, I think an easy-to-use drop in replacement library is the best one.
JH
On Apr 3, 2015, at 5:13 PM, Andrea Giammarchi [email protected] wrote:
Also last thought on this: by specifications, the User Agent is capable of dropping the request internally triggering a Fatal error so all this immutable Promise is a lie because it's just virtually effective in User Land but in core the state is mutable because that's needed
So how do we move on here, since deprecating fetch thread has been closed, and Tasks are not even close to be a specification standard?
—
Reply to this email directly or view it on GitHub.
Reminder that this thread is for discussing adding cancellation to the
request portion of the fetch API. Repeated off-topic whining from the same
couple of people will be deleted.
@jakearchibald hah, Jake. All discussion here is by those "same" people, do not you think? So, since it's thread about cancellation, can you provide feedback for all comment here which are not off-topic? This seems what here are few good proposals which might be considered and discussed.
We are not here to do any trolling, we are here to solve this problem because we will end playing and developing web sites/app with API, not you (here I believe you have no time for regular web-dev). So please, let's solve it. Some way or another.
I've been taking the feedback here (filtering out the off-topic
point-scoring stuff which is becoming increasingly hard), and working it
through real use-cases, and the es6 spec. I will continue to monitor this
thread too, and will post when I have something concrete.
I am also a web developer, building and maintaining multiple sites.
@jakearchibald we could talk about that, then. A standard for cancellable promises is not complete: (https://github.com/promises-aplus/cancellation-spec/issues). It seems like fetch, since it is so intrinsic, should not deviate from the standard when adding cancellation.
If it is non-negotiable to use Promises for this API, then it seems as though you should not include an abort semantic. If the abort semantic is non-negotiable, it seems as though you should not use Promises.
This thread has explored multiple options that use promises and are also
cancellable.
Little not-so-off-topic detail: as mentioned here could we also pick canceled instead of cancelled since it's less to write and more consistent with known standards such color VS colour ?
Yeah, that's my British fault, sorry. Didn't know it was different.
@jakearchibald Sorry, let me clarify. Do you think it's okay for fetch-based Promises to have different semantics from standard Promises, once the standard has cancellation added?
Hi Jake,
I hope you don't feel embattled here. :-( I really appreciate your efforts to improve the status quo of network APIs on the web. You are also clearly trying to leverage the tools available to you in the web platform, whether they be the browser or language (async/await).
I think that valid concerns have been brought up about each and every one of the cancellation proposals on the table. Given the issues called out in this thread, it shouldn't be surprising that cancelable promises are nowhere near a reality. Other than a little discussion, the specification repo hasn't really seen much activity over the last two years.
Do you favor any of the proposals currently under consideration?
JH
I've forked then/promise into ignorablePromise, and made it work the way I believe it should. This includes ignoring up a chain, calling an ignore method in the initial promise, and cancelling down a chain. This proposal uses reference counting and it attempts to cancel/ignore promises that make up the chain, but makes no guarantees. For example:
function greetMe(greeting){
return new Promise(function(resolve, _, onIgnored){
var token = setTimeout(() => resolve(greeting), 100);
onIgnored(() => clearTimeout(token));
});
}
simple: {
let a = greetMe('hello');
a.then(greeting => console.log(greeting));//this never happens
a.ignore();
}
refCount: {
let a = greetMe('hello');
let b = a.then(greeting => console.log(greeting, 'world'));//this never happens
let c = a.then(greeting => console.log(greeting, 'jake'));//this happens
b.ignore();
}
double: {
let a = greetMe('hello');
let c = a.then(greeting => greetMe('world'));
setTimeout(() => c.ignore(), 150);//this will ignore the greetMe('world') promise
}
resolved:{
let a = Promise.resolve("value");
a.then(r => console.log(r)); //this happens
a.ignore();
a.then(r => console.log(r)); //this never happens
}
This implementation attempts to cancel the work being done, but makes no guarantees that it will happen. Instead it makes every effort to ignore the result of a promise. When a promise has been ignored then from the outside it looks like a forever pending promise. To clean up, an ignored promise will ignore up the pending chain and it will cancel down the pending chain. A resolved promise that is ignored will not do anything up or down the promise chain; it will only affect promises created from it afterwards.
Hi @mariusGundersen , that looks like my first example except one part:
When a promise has been ignored then from the outside it looks like a forever pending promise.
I thought that was the thing developers were scared the most, having forever pending Promises around, hence my silent resolution that if undefined as value, it's pretty clear the resolution itself is useless, but not forever pending.
I do like the fact you used onIgnored in the way and same place I put onCanceled but I'd like to understand if forever pending is actually a real-world concern 'cause I might bake yet another version of my initial logic that simply ends up doing what your one does.
The way I see it, you need somehow a "flagged as forever-pending" state in order to let engines understand there's no reason to hold everything else ".thened" after ...
TL;DR why do you think forever pending is the way to go, and how can it let engines understand that's a meant forever pending and not a regular Promise to actually wait for?
On the other side, I see "forever pending" like a concatenation of operations through Arrays so that even if filter reduced it to 0, all others map and reduce in the queue will "vanish" without problems. Still we need a "forever pending" state ( and I also start thinking that .regret() would probably be even more appropriate term for this whole story )
After I made this I started thinking about that as well. Consider:
function load(url){
let a = fetch(url);
let c = a.then(b);
let e = c.then(d);
setTimeout(() => c.cancel(), 100);
return e;
}
In the above example the middle of the chain is cancelled/ignored/aborted, and the message that work should be stopped, that the result is no longer needed and that things should be cleaned up will travel in two different directions; up from c to either a or b, depending on if a has resolve yet, and down from c to e and beyond, to let them know that there wont be any value and that the promises should clean up so as many of the promises as possible can be garbage collected.
If a signal should be sent down the chain that it has been cancelled, then I don't think that it is the original promise (the top of the chain, either a or b in the code above) that should provide this value, since they don't know why it was cancelled. If a value is to be provided then it is the canceller that should provide this value.
The canceller is further down the chain, and the value it provides will travel down the chain, and so there is no need to send this value up the chain. That is, neither a nor b need to know why they have been cancelled. It might be that the promises down the chain want to know why, and so this value should travel down to e. d doesn't exist yet, so it doesn't need to be cancelled.
One option then is to have cancel()/ignore()/abort() be called with an optional parameter and have the promise resolve to that value instead of being forever pending. We could also let it be called with a function that either returns a value or throws an error, and have it fulfil in the first case and reject in the second case. But this introduces a way to inject a value into a promise chain at any point, even after it has resolved, which I don't think is a good idea at all! Then the method shouldn't be called cancel() but resolve()/reject(), and it makes cancellable promises something completely different than normal promises.
This can also be implemented using nested chains:
let e = ((url, cancel) => {
let a = fetch(url);
let c = a.then(b);
return Promise.race([c, cancel]);
})(url, cancel);
let e = c.then(d);
The only remaining option is to have a third possibility for the promise then method; onCancelled:
somePromise.then(onResolved, onRejected, onCancelled);
Again, if a value should be passed down the chain to this onCancelled method, then I think that should be from the canceller, not from the cancelled promise. But this introduces complexity in whether or not this method behaves like the two other, ie can you return and throw in it and what happens if you return a promise?
I think my first playground allows you to pass on p.cancel({reason:"because"}) and silently resolve using that object after canceling the chain the way you describe. Have you tried it?
I wonder if there's any progress on this.
I also would like to bring to your attention that even Dougla Crockford somehow ditched Promises in favor of cancelability:
This is from his latest rq library
This pattern is so problematic that some of its users have denounced asynchronicity, declaring that it is unnatural and impossible to manage. But it turns out that the problem isn't with asynchronicity. The problem is trying to do asynchronicity without proper tools. There are lots of tools available now, including promises.
There are many good things that can be done with promises, but promises were not designed to help manage workflows in servers.That is specifically what RQ was designed to do. Asynchronicity is our friend. We should not attempt to hide it or deny it. We must embrace asynchronicity because it is our destiny. RQ gives you the simple tools you need to do that.
What DC is proposing is something similar to my proposal: an optional way to specify cancelability of an asynchronous _contract_.
If I ask a friend to do me a favor, but it takes long time and I don't need it anymore, I'd like to know my friend is capable of dealing with me changing my mind: it was me asking him to promise something, if I say "you know what? I don't need it anymore" I'm pretty sure he'll drop the task and do something else.
Please let us know if anything is changing or moving at all, this is also a very important point of my book and I'm sort of stuck because I don't want to go out with an already updated chapter if anything changes in here at all.
Thanks for helping out and best regards.
We're currently looking into two solutions: Cancellable tokens and cancellable promises. I'm going by the following requirements:
.finally(func), along with .then(onfullfill, onreject, onfinally) to mirror the sync counterpartonfullfill or onreject, but will call onfinallyevent.respondWith in a service worker)Have I missed anything?
I'm still a fan of the ref-counting approach, but it creates a tricky situation and I'm open to suggestions… Take the following nonsensical example:
var fetchPromise = fetch(url);
var jsonPromise = fetchPromise.then(r => r.json());
var fetchPromise2 = fetch(url2);
var waitingPromise = fetchPromise2.then(_ => fetchPromise);
// Ref counts
// fetchPromise: 2
// jsonPromise: 0
// waitingPromise: 0
Imagine fetchPromise has yet to settle, fetchPromise2 has fulfilled with a Response, waitingPromise has resolved with fetchPromise. Then:
waitingPromise.cancel();
waitingPromise cannot be marked for cancelation as it has resolved. But since it hasn't settled, its resolved value, fetchPromise has its ref count decreased by 1, to 1.And there's the problem. Although .cancel was called on this unsettled promise, it continues to resolve with fetchPromise.
This wouldn't be a problem with:
var fetchPromise = fetch(url);
var jsonPromise = fetchPromise.then(r.clone() => r.json());
var fetchPromise2 = fetch(url2);
var waitingPromise = fetchPromise2.then(_ => fetchPromise).then();
// Ref counts
// fetchPromise: 2
// jsonPromise: 0
// waitingPromise: 0
…as waitingPromise hasn't resolved due to the added .then(). I think we either need to find a way to do something like that in the spec (maybe it does already?), or allow a resolved but unsettled promise to cancel. Both of these would also help if a CancelablePromise resolved with a foreign thenable.
but promises were not designed to help manage workflows in servers.
Lol, looks like someone needs a history lesson
@jakearchibald thanks for the sum-up. If I understand correctly, finally introduces a third state, exactly same way canceled would do. You said canceled won't call onfullfill neither onreject so I assume it will be resolved silently through onfinally, is this correct?
I also read your answer as "_it won't happen any time soon_" 'cause it's more elaborated than any proposal to keep it simple we had here ... this is a bummer for me, but I guess the only way to move forward with this issue.
Last question would be: you mentioned CancelablePromise again so I am assuming Promises as these are will never be cancelable ... accordingly, if we need a new specification, wouldn't be better to just move away from Promises and bring Tasks natively in?
If I understand correctly,
finallyintroduces a third state
Kinda. "cancelled" would be the third state.
You said canceled won't call
onfullfillneitheronrejectso I assume it will be resolved silently throughonfinally, is this correct?
To the outside observer, yes. So you'd write:
showSpinner();
fetch(url).then(r => r.json())
.then(displayData)
.catch(showError)
.finally(hideSpinner);
If the fetch errors, an error is shown and the spinner stops. If the fetch succeeds the data is displayed and the spinner stops. If the fetch is cancelled the spinner stops.
I also read your answer as "it won't happen any time soon"
I'm not sure what your timelines are, but it won't be rushed into.
Last question would be: you mentioned
CancelablePromiseagain
The token approach may be able to be added to Promise, but I think adding .cancel to Promise could break existing code.
wouldn't be better to just move away from Promises and bring Tasks natively in?
Could you point to a implementation of tasks you're particularly fond of?
I think adding .cancel to Promise could break existing code.
How is that since nobody using Promises these days woul dpass a third callback + nobody believes it's possible to cancel them?
Could you point to a implementation of tasks you're particularly fond of?
I'm not fond on any particular implementation, all I know is that Tasks manage cancelability in core and by default, I'm sure these have been discussed already, and I'm sure there's need for them otherwise developers would just use Promises.
What I'm not sure about, if it has been even proposed as standard by anyone. So I'll let eventually others suggest the best Task based option.
@WebReflection @jakearchibald this thread has points about Tasks https://twitter.com/jlongster/status/585526703298109441 by @jhusain
And, of course, he also was here with his proposal about Task, so we can easily refer to that implementation, can't we?
It seems like you have covered all the important requirements. I like that you have introduced a finally rather than a onCancelled callback, as this simplifies things. I assume the finally callback doesn't take any arguments?
then().finally(function(){
assert(arguments.length === 0);
});
And there's the problem. Although .cancel was called on this [waitingPromise] unsettled promise, it continues to resolve with fetchPromise.
This can be solved by having the promise that .cancel is called on ignore any result given to it. So waitingPromise in your example would mark itself as CANCELLED so that even if the promise it is waiting for is resolved, itself wont resolve. In addition it calls cancel() on any promise that is is waiting for (if the promise it is waiting for has cancel defined).
The advantage with this is that it can work even without CancellablePromises:
const a = fetch(url);
const b = a.then(r => r.json());
const c = b.then(json => somethingThatReturnsAnOrdinaryPromise(json));
c.cancel();
If both a and b are resolved CancellablePromises, then cancelling c will try to cancel the result of somethingThatReturnsAnOrdinaryPromise, which can't be cancelled. But it will still ignore the result, so from the outside it will look the same. This makes it possible to combine CancellablePromises with existing Promise implementations without worry.
BTW, this would work pretty well with async/await:
async function ajax(url){
showSpinner();
try{
const r = await fetch(url);
const json = await r.json();
await displayData(json);
}catch(e)
showError(e);
}finally{
hideSpinner();
}
}
var p = ajax('/test')
setTimeout(() => p.cancel(), 100);
@WebReflection
How is that since nobody using Promises these days would pass a third callback + nobody believes it's possible to cancel them?
Consider:
var getValue = (_ => {
var p;
return _ => {
if (!p) {
p = new Promise(r => setTimeout(r, 5000)).then(_ => "Hello world");
}
return p;
}
}());
var p1 = getValue();
p1.cancel();
var p2 = getValue().then(v => console.log(v));
p1 is cancelled, but that means p2 is also cancelled. Without cancellation, p2 would always successfully resolve. Introducing this change directly to Promise feels a bit dangerous.
Introducing this change directly to Promise feels a bit dangerous.
... again, how is that since nobody cancels Promises these days ? Where is the dangerous part? If that returns suddenly a cancelable Promise, what would change?
The reason I've asked is to understand if Fetch API is doomed forever or if it will be improved through a cancelable Promise.
@jakearchibald I believe forever pending (or with state "finally") promises do not throw any errors and do not leak any data, so worst thing which might happen is that someone may expect p2 to be resolved, but it won't. But it's a bit strange because if someone uses cancel-ability on promises or library with such ability, then one should be aware of such situation.
how is that since nobody cancels Promises these days?
In my example, the "library" code is written assuming the promise will always resolve. If we add cancellation to existing promises it breaks that assumption, and the library will have to update. I don't know if that's a good move.
The reason I've asked is to understand if Fetch API is doomed forever or if it will be improved through a cancelable Promise.
It will be improved through a cancelable Promise (or something similar). You can't use fetch without already guarding against failure, so cancellation can be added here.
@NekR @jhusain is there a particular implementation/spec of Task that you think would work well here?
@mariusGundersen
I assume the finally callback doesn't take any arguments?
Correct. Same as with sync code.
This can be solved by having the promise that .cancel is called on ignore any result given to it. So waitingPromise in your example would mark itself as CANCELLED so that even if the promise it is waiting for is resolved, itself wont resolve.
Unless I'm understanding the spec wrong, I think the problem is that waitingPromise _has_ already resolved, but it's resolved with a promise, so waitingPromise hasn't yet settled. Allowing a promise that has resolved but not settled to cancel could solve this, but that might have a greater impact elsewhere.
BTW, this would work pretty well with async/await:
This requires async functions to return cancellable promises.
@NekR that's fair. I just worry that adding this to existing promises is going to take even longer to build consensus on. Also, you'd need to invent a new way to provide a promise that cannot be cancelled by the receiver, which seems to be important to people on this thread.
in all my examples that's straight forward: you don't provide a way to cancel it => Promise non cancelable. Is this too pragmatic?
The only implementations I'm aware of are @kriskowal 's and my own.
Kris: https://github.com/kriskowal/gtor/blob/master/task.js
Mine: https://github.com/jhusain/task-lib
It would be great to discuss the benefits and drawbacks of these two approaches. Currently I feel like either of these types would be a better option than trying to morph Promises into a type with fundamentally different guarantees.
I share your concern that trying to bolt cancellation on the promises may actually be less expeditious than introducing a new primitive. This change would presumably need to be approved by both the WHATWG and the TC-39, and promise cancellation is clearly not without controversy. :) It may be more expedient to try and get a new, more appropriate type standardized and let Promises be Promises. Moreover I'm sure we'd end up with a better API.
those in ES6 with non cancelable promises will never be affected because those Promises won't be cancelable and, in the old logic, won't be canceled since nowhere the code will try to.
In case it does, those will be just silently resolved or forever pending so no code will ever be concretely affected.
As summary:
But the problem here is that we cannot "aside entirely Fetch" becaus it's specced as an API that returns a Promise.
There has been a lot of ink (ok, electrons) spilled on this topic already, but I don't think anyone's brought this up.
People claim that cancelling has no analogue in synchronous code, but I think it does: interrupts. When you interrupt a thread, you basically cause any blocking operation in that thread to fail, and that interrupt propagates through the call stack as appropriate. In the case of promises, if you cancel someone at the end of the chain, it causes the head of the chain to fail. In other words, in this code:
let a = fetch(url);
let b = a.then(...);
let c = b.then(...);
let d = c.then(...);
d.then(v => console.log("Success"), e => console.log("Failure"));
d.cancel();
The most appropriate thing to do is to abort the URL fetch and have "Failure" get printed to the command line.
The problem of course is if two people tried to say a.then(), which comes down to a question of if "cancel" means "kill" or "ignore." One could sidestep the issue by banning then on cancellable promises from being called more than once, but I'm not sure if that's still feasible. For simplicity, though, I do like the idea of CancellablePromise.cancel as being defined as "take the 'original' promise, call reject(ABORTED_PROMISE) and then cancel() for the values of reject and cancel provided in the promise constructor.
I prefer the use of a special well-known exception for cancelling instead of a promise method, since it makes it possible to approximate async functions with a wrapper around a generator, which is not possible with only a .finally method to catch cancellations.
Fetch API is specd as Promise, either we change this or we bring .cancel in: pick your poison
I'm all in favor of .cancel as finally 'cause it has a synchronous counterpart and makes Tasks let's say less needed. I also think that if I am asking you to promise me something, I should be able to tell you I regret I've asked that and you don't have to fulfill it ... but again, philosophy and synchronous counter part aside, we have a problem here, and we should solve it ASAP.
Unless I'm understanding the spec wrong, I think the problem is that waitingPromise has already resolved, but it's resolved with a promise, so waitingPromise hasn't yet settled. Allowing a promise that has resolved but not settled to cancel could solve this, but that might have a greater impact elsewhere.
No, not according to the A+ spec. It specifies how to handle a promise of the same implementation and a promise of a different implementation. In the first case it adopts the state, which is done to reduce the memory footprint of promises (see this discussion thread). A CancellablePromise can chose not to do the optimization of adopting the state, but always use the 2.3.3 algorithm to get around this. It also won't have the memory leak issue, since a chain of never resolving promises can be cancelled from the outside, which will unravel the chain. My implementation does this.
@martinthomson elaborating on your idea, why not simply:
let abortFetch;
let p = new Promise(resolve => abortFetch = resolve);
fetch(url, p).then(success, failure);
// on error
abortFetch();
causing fetch to reject with CancelledError?
This is a long old thread by now, but to throw my 2 cents in, I think morphing a promise into a control object is a terrible semantic idea in any incarnation. Today a promise can be passed around to several functions without granting control to any of them. Nevermind that any function acting on said control might affect all the other ones (action-at-a-distance). Control has to live upstream in the tree, and it's not for the recipient of a promise to break, resolve or reject it. As tempting as it may seem to overload the lone returned object, this only seems beneficial in the most naive uses where the initiator and the consumer are the same, and wont work for much else. It'll also complicate wrapping and libraries to no end. Control is an input, a promise is an output. Controller pattern for the win.
I'm not confused about Promises. I think they are easy to understand today. A relatively large community understands them and their de facto standard status is one of the reasons why they were integrated into JavaScript.
The current promise cancellation proposal is not a de facto standard. A small number of people thought it up a few weeks ago. We have no evidence that it will be easy for developers in the community to understand. I think it's fair to say that it adds a good deal of complexity to Promises, and there's no guarantee that the wider community will easily understand it.
Today there are more people who use Observable than cancelable promises. That number is likely to increase sharply in the near future, because Angular 2 and React both currently plan to use Observable to model data fetches. Both these teams took a look at Promises and decided that they were inadequate for a fetch API. Why not change fetch to return an Observable and let Promises continue to be a well known quantity? Observable is moving through the JavaScript standardization process. There is already an interoperability spec being used by multiple frameworks.
Just wanted to add another piece of information in support of my suggested "controller" object pattern for fetch()'s return value: Revocable Proxy.
Proxy.revocable(..) returns an object with two properties, the proxy itself and the revoke() function. This is exactly analogous to what I'm suggesting should be the return value from an fetch(..) call, except instead of revoke() we should call it abort() or cancel(), and instead of proxy it's promise.
Object destructuring makes this still super easy to deal with:
var {promise: p1} = fetch(..);
var {promise:p2, cancel} = fetch(..);
// later... `cancel()`
Okay, as this goes no where, can we have at least this simple version for now, so, for example, polyfills could implement it?
fetch('url', function(abort) {
outerAbort = abort;
});
And just do not care about ref-count, etc. Cancel request associated with that fetch and just never resolve promise associated with that request. If promise already resolve, then, just do nothing, as abort works on xhr.
@jakearchibald @WebReflection @annevk does anyone has serious arguments against it?
@NekR we'll take the time needed to solve this properly, it's not a life-or-death situation.
I have read through most of this discussion, and while I don't typically like to be involved in these things, I feel compelled to ask why there is so much discussion around "cancelable promises" while nobody has brought up simply adding an abort() method to Request objects. I've seen a lot of people say the words "cancel the request", but I haven't seen that said the form of an API.
As @getify has been arguing all along, and as @benjamingr quoted @kriskowal ("If any dependee cancels a promise, it would be able to interfere with future dependees"), a promise represents a end result (or an error that occurred in trying to get that result). You should be able to freely pass that promise of a result around without fear of one consumer ruining the party for everyone else (i.e. tight coupling, which we all despise). The promise should absolutely remain externally immutable.
It seems fitting that only the creator of the request should have the power to cancel it. That's the beauty of giving the Request object the power of abort() (or cancel() if you prefer). Obviously, calling fetch('/some/url') would not have it, but in the simplest cases you don't typically need to abort a request anyways.
The more interesting discussion would be what happens when a request is aborted? If the promise hasn't resolved a response, should it be rejected with a special error type or should it be resolved to special response type (just like the "error" response, that has its status set to 0 and its body set to null)? I would vote for the former (reject) so it could be consistent with the response's "body" promises (text(), json(), etc.) being rejected with the same error type, but my opinions are not nearly as strong there.
I really hope you guys see the light and don't create a CancelablePromise type to solve this problem! :sweat_smile:
As @getify has been arguing all along, and as @benjamingr quoted @kriskowal ("If any dependee cancels a promise, it would be able to interfere with future dependees"), a promise represents a end result (or an error that occurred in trying to get that result). You should be able to freely pass that promise of a result around without fear of one consumer ruining the party for everyone else (i.e. tight coupling, which we all despise). The promise should absolutely remain externally immutable.
@appden Promise cancellation isn't planned to be like this at all. If you have many consumers for something, then all of them need to cancel for the operation to be cancelled. But if you have say only 1 canceller and 2 other consumers and the operation completes, the 1 canceller won't get their callbacks called (the cancellation was successful as far as they were concerned).
Promise cancellation isn't planned to be like this at all.
I'm not sure where this assertion is coming from, but there's a whole bunch of messages in this thread that directly contradict what you're saying.
All the messages I've posted here about being concerned about cancelable-promise having "action-at-a-distance" characteristics are because of the many assertions here that just one promise way down the end of the chain could call cancel() and that propagates all the way up to the fetch(..) instance to cancel it, meaning any other peer promises would be "affected" in that they wouldn't get to wait for fetch(..) to finish.
The concern @appden echoes is quite clearly evident from the majority of messages in this thread on that topic.
@appden
It seems fitting that only the creator of the request should have the power to cancel it.
which is exactly what everyone proposed here. You create the promise and still you optionally define its cancelability.
In my code/examples it's me defining it, in fetch case would be the vendor defining it. Call it abort, if you prefer, the concept is that since you are the creator and you have the rights to decide abortability, why would not give the ability to expose this decision?
If you don't want to expose such cancelability, but you define it for your own code, you can wrap it in a non cancelable Promise and pass around that one instead.
Obviously, calling fetch('/some/url') would not have it, but in the simplest cases you don't typically need to abort a request anyways.
that's usually what libraries are for, not core APIs. Latter should be as easy as possible and yet provide coverage for all cases too ... and here is where this problem was raised and yet not a concrete solution.
Anyway, the TL;DR of this thread is indeed the following:
a promise represents a end result
Which is a footgun for anything asynchronous network related. Again, for some reason fetch cannot change its returned type which is already specified as Promise, so here somebody tried to put a line between abstracts and real-world needs, proposing solutions.
It apparently ended up that behind the scene, the core API is indeed capable of aborting a Promise ... of course it's needed, of course abstract and pattern purism is less important than concrete needs ...
However, I believe this thread could be simply closed because the solution is probably been discussed elsewhere since there's no progress whatsoever, and every single time any of us replies is not adding anything new to this discussion, or clarifying anything else, to anyone involved, since everyone understands everything already said, it's just a matter of applied or avoided-at-all-costs pragmatism.
I'm indeed, for everyone happiness, officially done here, unless somebody calls me in explicitly.
Best Regards ... and let's hope it won't end up worst than this:

@WebReflection +1 the sheer amount of debate here discourages people from having a discussion about it. I have full confidence in what you guys are working on as well in Bluebird 3.0's cancellation - good luck and keep us updated if you want people to test stuff for you :)
Since network is unreliable, callers are expected to handle failures in a graceful way. I don't see a huge difference for the chained promises between abortion of a request and unplugging the network cable.
There are two implementers of fetch: Chrome (current stable, 42), Firefox (will be enabled by default in 39), so a quick resolution of this issue is needed.
Note for (library) developers: I found that unloading the document or calling stop() in Chrome stops all network requests, including fetch(). Firefox still continues with the request though (see #53 and https://bugzilla.mozilla.org/show_bug.cgi?id=1165237). This could be used until the gap of the missing .abort() method is filled.
I'd put the abort() method on a controller that is passed to the fetch method (e.g. as a part of Request or RequestInit). If re-use of these parameters is desired (https://github.com/whatwg/fetch/issues/20#issuecomment-74236024), we could require the controller to be used only once, for one fetch call.
Promise cancellation isn't planned to be like this at all. If you have many consumers for something, then all of them need to cancel for the operation to be cancelled.
@petkaantonov That's still action at a distance. It effectively means that if I do var p = fetch(url); and I want to be able to cancel the fetch then I can't send p to anyone!
var p = fetch(url);
foo(p);
p.cancel(); // Will this cancel the fect? Depends on what foo does!
That seems broken. Just because I'm calling foo doesn't mean I want to grant it that power.
If you don't want to expose such cancelability, but you define it for your own code, you can wrap it in a non cancelable Promise and pass around that one instead.
@WebReflection It's enabling action-at-a-distance by default, burdening people who don't care, don't think about, or don't want cancelability with extra work to avoid unintended consequences (footgun).
@jan-ivar it doesn't have to be like that, it could simply be fetch('page', {cancelable: true}) so by default is a Promise, otherwise is something similar that exposes a cancel method. Even without chainability .... lets say a specialized Promise with a self contained ability to be dropped by the owner, not by anyone which the object is passed around.
@jan-ivar And how do you know that any method (e.g. .then()) you call on the object you just passed somewhere wasn't overridden and now does something completely different?
@petkaantonov then you can overload .then() to do what you want, and we should be done. ;-)
Trying to prevent footguns is different from preventing holdups. How much I trust foo is up to me, but that's no reason to break down guarantees further, and why immutable promises is a good thing. The system should help foo behave, not put out trip wires.
This example is perhaps better, so thanks for pointing that out:
var p = fetch(url);
p.then(() => foo());
p.cancel(); // Will this cancel the fetch? Depends on what foo does!
actually foo hasn't even executed when you call cancel there. But even if it was, p.cancel() would still cancel the operation. p is the root promise that doesn't have any "peers". For instance:
var a = getPromise();
var a1 = a.then(...);
var a2 = a.then(...);
var a3 = a.then(...);
canceling a directly (a.cancel())would cancel the operation regardless what the children do. But if you do a1.cancel(), it doesn't cancel a unless a2 and a3 cancel as well (its peers).
@petkaantonov Thanks for explaining that, I have a clearer picture now. But even if we can explain this to people - this little p can kill things, this little p cannot - I see what I think are fundamental problems:
If a1 cancels, and a2 cancels, and a3 cancels, ... why does it follow that a should be canceled? This assumes there'll be no more a.then()'s, when I can call var a4 = a.then(...); at any time.
Also, how to fork a chain without blocking cancelation? Say getPromise() has internal stuff it needs to finish after completion:
function getPromise() {
var p = asyncImplementation();
p.catch(() => {}).then(() => cleanupStuff());
return p;
}
This works fine today, but would now block cancellation. E.g. say you have this long chain where g1 is the result of getPromise():
\
g2
Unintended action-at-a-distance, and hard to find why things upstream aren't canceling.
It seems my comment triggered more argument about cancelable promises being a valid or invalid construct, but did not instigate much discussion about my proposed solution. I'd still like to see some feedback on my suggestion of adding an abort() method onto the Request object. It seems that the Request object is already not really meant to be reused since it implements the Body interface, which includes a bodyUsed attribute. I believe it's worth solidifying that a Request object may only be used once, even for GET requests, so that we can add the abort() method and perhaps even for attaching event listeners in the future as well (such as upload progress).
No matter what your opinions on cancelable promises, I believe it would very hard to argue that this API would be straight forward and not a "footgun".
I agree with @appden. If one of the two below were supported, there'd be no need for any of this discussion on cancelable promises.
var request = new Request('http://example.com');
setTimeout(function() {
request.abort();
}, 10000);
fetch(request);
_or_
fetch('http://example.com', {
terminator: function(abort) {
setTimeout(abort, 10000);
}
});
And library authors could pass along this "abort" control if they wished.
And it would seem sensible to me if the above promises were rejected with an error indicating end-user termination.
request.abort() doesn't compose so well with the storage of request objects. Also, fetch(request).then(r => r.json()) - should request.abort() be able to reject the read of the stream by r.json()? A terminator function has the same issues.
The goal is for…
var p = fetch(url).then(r => r.json());
// sometime later…
p.cancel();
And cancel will cancel whatever operation is in progress in that promise chain, be it the request, or reading the response.
@jakearchibald - I think this conflates the owner of the chain with a consumer of the chain. Literally, even: p looks like it's being assigned an entire chain, so it must be its owner, right?
Unfortunately, that semantic is lost on the language, so when we break it down:
var p1 = fetch(url);
var p2 = p1.then(r => r.json());
p2.cancel();
It becomes much less obvious to me that pc2.cancel() should cancel the fetch, as control would be swimming upstream. throw doesn't even have this power. While it might be effective, it's unintuitive, and not without surprises:
Control is foiled at the first junction point it finds upstream. There are none in this example, but as I showed 27 days ago, in general you have no way of knowing all junction points - _and they should not even concern you_ - which can lead to unintended consequences and hard-to-find bugs ("action at a distance").
Today, promises can be shared with and distributed to other code without giving up control. Changing this would be a surprise, and make it harder to reason about who has control.
Worse, it can lead to fear of sharing promises with other code, because control is now implicitly shared where before it wasn't, affecting how people design.
This is retreading ground already gone over at least once in this thread.
Sorry, I must have misunderstood the intent of reiterating the goal. Is that still the goal?
Thoughts that I have reading this:
Promise cancellation has a couple of problems:
The controller pattern proposed is similar to, but more restricted than, the Context pattern used in Go (https://godoc.org/golang.org/x/net/context) -- would that be usable here?
I just ran into this. I'm trying to write a ServiceWorker that takes existing long-polling requests - possibly from multiple tabs - and multiplex all the tabs into a single long-polling request chain.
This fits extremely neatly into the "progressive enhancement" story of ServiceWorkers - if the worker is not installed, things will work normally, just with extra network traffic.
As ServiceWorkers do not have access to abortable requests (XHR is not available + this issue), when a client subscribes to a new channel by xhr.abort()ing the previous request and starting a new one, the worker is forced to let the old (stale) fetch continue but ignore the result.
If a client is subscribing to a lot of new channels, this leaves quite a few stale network requests around.
In the WIP code's architecture, it would be much more convenient to store a controller / context object like so:
function restartPolling() {
// ...
if (previousRequest) {
previousRequest.abort();
previousRequest = null;
}
// ...
fetch(...)
// ...
}
I hope that giving an actual use-case will help to make a decision.
I think this is the only way:
var request = new Request('http://example.com');
setTimeout(function() {
request.abort();
}, 10000);
fetch(request)
.then(statusHandler)
.then(resposeAsHandler);
.catch(abortCatchHandler)
And we can use catchable exception in thenable chain. This will prevent for calling then handlers and the exception can be catch in Promise.prototype.catch. But after catch the chain is restored and developer must know it. Also a don't know how to design in Promise pattern an exception, that throws in request.abort method in example above.
Imo promises is an abstraction to make async code look sync by providing it with return/throw/try/catch semantics. But if the abstraction doesn't fit the use case, than something else than promises should be used, like observables.
Ben Lesh gave a great talk about observables at ng-europe last month and why promises just don't work for ajax requests and why Angular 2 went with Observables for their http module https://www.youtube.com/watch?v=KOOT7BArVHQ
Cancellation isn't the only problem. What about progress? What about retry mechanisms?
So, what's the status on this? What remaining concerns are blocking this from being specced? Why not have fetch() return an object with:
then(cb), which takes a resolution handler and returns a CancelablePromisecatch(cb), which takes a rejection handler and returns a CancelablePromiseabort(), which sends rejection to descendent promises the same way an error in the request wouldcancel(), which registers a disinterest in the results of the fetch, which will cause the fetch to be silently aborted if all CancelablePromises following from the fetch call cancel() and the fetch is garbage-collectable (script authors should consider this an optional optimization and implement a strong-reference-counting abort if guaranteed request cancellation is necessary for perf)addEventListener('progress',cb) (and the corresponding removeEventListener), for managing progress event listeners which receive ProgressEventsWhere CancelablePromise has:
then(cb), which takes a resolution handler and returns a Promisecatch(cb), which takes a rejection handler and returns a Promisecancel(), which registers a disinterest in the results of the fetch, which will cause the fetch to be silently aborted if all other CancelablePromises following from the parent fetch call cancel()Because cancelable promises have all kinds of problems. If we're going to do anything here at this point, it's going to be either a set of methods on the returned promise and only that promise (they don't chain), or a separate object with those methods.
OK, so we can punt on cancel() and just go forward with abort(). My point is, it's been _over a year_ since #20 was posted, and we _still_ don't have _any way whatsoever_ to monitor or terminate a request made via fetch().
That's not true. https://jsbin.com/gameboy/edit?js,console streams a network response, monitors it, and aborts it. This works in Chrome today.
The missing piece is aborting before headers arrive.
@jakearchibald really nice example :+1:
Still, aborting the request before the headers arrive is a valid use case. I think even just putting an .abort on a Request and having abort semantics (rather than disinterest) and throwing an AbortedError could solve it for 99% of users.
Personally, I'd love to see sound disinterest cancellation semantics on promises - and they'be been working out really well for bluebird. The problem is that it is _a lot_ of work to do.
aborting the request before the headers arrive is a valid use case
I'm not suggesting otherwise. Like I said it's a "missing piece", not an unnecessary one.
As for abort on request, I'd rather not add this until we figure out the best way to handle progress on the request side (progress on the response is a solved problem). The two solutions may go hand-in-hand.
Roger, sounds good to me. Any ETA on this? I think communicating things that are blocking this from moving forward would help reduce some angst some vocal parts of the community feel about fetch .
The integration of streams and fetch is very active right now - just finishing off a post on some of the stuff that recently landed in Canary.
Developer angst is a constant I'm afraid. We preach iterative development and deployment, but we act with great indignation when we're the customer of that approach.
Cool, good to know. A blog post could indeed be helpful in making the process more transparent.
The post is written from already fully-transparent sources.
https://github.com/whatwg/streams
https://github.com/yutakahirano/fetch-with-streams
Blink-dev etc.
Perhaps transparency wasn't a good word choice on my end there. I think "accessible" is more suitable as the vast majority of users (even those using fetch) are probably unaware of these documents and most of them are probably also unable to read them easily. Thanks again.
@benjamingr you are right, I think we failed in clear communication here. It has been almost a year, but we still want a solution that is good and fits well with the API. @domenic and @jakearchibald who are somewhat leading the charge here have ideas on how to solve this through cancelable promises. Development of that is taking a little longer due to other work taking priority, such as streams and <script type=module>, but we have not given up on pursuing that direction and will hopefully get to it soon.
Furthermore, if there are other issues on which you seek clarification, I think we'd be happy to blog more on blog.whatwg.org and elsewhere. It's not always clear to me personally what is clear and unclear to the rest of the world.
@benjamingr here's the article on streams https://jakearchibald.com/2016/streams-ftw/
Great, thanks. I'll be sure to update any posts I have on StackOverflow accordingly :)
@annevk, it seems you should close this issue.
@monolithed no, as there is still no way to abort a fetch before the response body has started streaming in.
This is a _lot_ to wade through, so apologies if this has already been answered... But what happens in this scenario?
datadata.cancel()then call.If it's cancelled after it's resolved, and then you then it out again, do you still get the value?
What happens if you cancel while processing the resolution? Will the following still log a value? I mean, I'd _think_ it would, but I really don't know.
cancelablePromise.then(x => {
cancelablePromise.cancel();
return x;
}).then(x => console.log('here', x));
For all of the times I've derided Promises for not being cancelable, it's the lack of cancellation that is one of Promise's strongest traits. The name even says it, it's a "Promise", it's a guarantee. Adding cancellation would pollute the type and have implications for any "thennable". I think @jhusain was onto something with the idea that it should be a different type. Not necessarily Promise-shaped, and probably not an Observable either. Something more like a single value Observable-type-thing.
@blesh I doubt this would get much attention here since how to do cancellation at the promise level for fetch hadn't been decided on yet - as mentioned above.
You're welcome to ask it in the bluebird repo though.
@blesh by the way, that single values but unicast primitive you speak of is what Kris calls a task and is what I suggested in here in the first place.
That said, in bluebird semantics if you opt in to cancellation promise cancellation has _disinterest_ semantics - like refcounted unsubscribe in Rx. Cancellation describes you are no longer _interested_ in the value but you can't "abort" it - this is also discussed a little in this thread.
In a disinterest model, calling cancel on a promise means "I'm not interested in that promise's result" ad not "do whatever it takes to destroy the data for subscribers". Once a promise resolves - calling .cancel on it is a no-op, you can still access the then'd value. If you cancel a promise while subscribers are waiting for it - the then handlers won't run but finally blocks and disposers would. So:
If it's cancelled after it's resolved, and then you then it out again, do you still get the value?
Yes.
What happens if you cancel while processing the resolution? Will the following still log a value? I mean, I'd think it would, but I really don't know.
In this example, you cancel it _after_ resolution (as cancelling it _while processing_ the resolution isn't really possible) which is a no-op, the console.log will execute.
Adding cancellation would pollute the type
As always - anything added dilutes everything else. I agree that adding cancellation pollutes the type in a sense but it's a necessity for client-side code. Fetch will need to be cancellable regardless of promise cancellation, either with direct cancellation, with tokens or even with a cancel method on the Request object.
Ref-counting semantics in JavaScript sounds like a terrible idea. What's next, pointer semantics?
wat
@jan-ivar you need to manage resources somehow explicitly since well: the whole point of cancellation is just that.
You can either do it with automatic reference counting or you can do it with manual reference counting and deletes. Allow me to emphasize this _there is no solution that does not require book keeping_ since that's the whole point of cancellation. Between _manual_ and _automatic_ bookkeeping I'd rather have automatic.
I see nothing automatic about requiring every single reference-holder to signal disinterest before an action is cancelled, I see bugs. Promises are also not solely about relinquishing resources, but for asynchronous operations with often observable side-effects, cementing action-at-a-distance as a pattern. I would ask what use-cases merit distribution of control in this manner.
It's not about automatic vs. manual, it's about giving up control for no benefit, inviting bugs.
I see nothing automatic about requiring every single reference-holder to signal disinterest before an action is cancelled
The automatic part is that the library keeps track of the references for you and disinterest semantics are preserved.
I would ask what use-cases merit distribution of control in this manner.
Uh... you're in a thread asking how to abort a fetch, there are tons of cases where you want to signal disinterest early - io is expensive - especially on the client and minimizing that is something that users should be able to do.
Promises have multicast semantics but are typically used in a unicast way - typically there is a single subscriber for most promises so calling .cancel on them aborts the request. For the cases you want multiple subscribers - you can't abort the request unless _all_ of them want to abort it without breaking the abstraction horribly and getting unsound cancellation with reject semantics.
For single subscriber cancellation is trivial with promises anyway and abort and cancellation semantics are indistinguishable.
For multiple subscribers - abort semantics don't work with promises at a very very fundamental level, they don't fit with the abstraction. Disinterest semantics fit in quite naturally. As far as the _consumer_ is concerned they merely signal they are disinterested in the resource - if no one else is - the request for it gets aborted. If others need it - it doesn't. As a consumer - you don't have to _care_ about refcounting at all.
Also, we've been using it for a while now since bluebird 3 came out and your point about bugs is completely unfounded. Older versions (like bluebird 2) had much quirkier abort semantics which were harder to reason about.
@benjamingr I'm glad you've arrived at the same conclusion I have that what you call abort semantics just wont work with promises. My concern with the more benign ignore semantics you mention is that it I see little technical difference - the lack of a way to consume one of these promises without implicit interest is still a problem - I see merely a recasting of expectations, with little guidance for folks looking for the cancellation hammer.
You make a distinction between single and multiple subscriber promise-use as if they're two ways of working that users can choose between. But users compose promise-chains from calls to functions from all over the place that they don't control, and so they cannot guarantee that the net promise-chain is free of multiple subscribers (now or in future version of libraries), as I mention in https://github.com/whatwg/fetch/issues/27#issuecomment-102500578. Users also shouldn't have to care about how functions are implemented in this regard, so such an expectation breaks an important abstraction.
There are also plenty of ideas in this thread that don't require distributing control indiscriminately.
My concern with the more benign ignore semantics you mention is that it I see little technical difference - the lack of a way to consume one of these promises without implicit interest is still a problem
Consuming a promise is very explicit interest in its result.
I see merely a recasting of expectations, with little guidance for folks looking for the cancellation hammer.
The thing is - when we looked at actual real use cases no one is really looking for a "cancellation hammer". All the use cases we looked at (for example XHRs) work beautifully with disinterest cancellation semantics. That is - I looked at hundreds of projects w.r.t cancellation and answered over half a thousand promise question on Stack Overflow and have never seen someone who needed a "cancellation hammer" or even wanted one. Abort semantics are very frowned upon in other languages and systems too. Even in other primitives (like observables) it is _highly_ discouraged to have unsubscribe with multiple subscribers without refcounting.
You make a distinction between single and multiple subscriber promise-use as if they're two ways of working that users can choose between.
Users don't need to care, cancellation would "just work".
they cannot guarantee that the net promise-chain is free of multiple subscribers (now or in future version of libraries)
Of course, probably the vast majority of libraries returning promises wouldn't even support cancellation - the nice thing with this model is that with disinterest semantics that can be incrementally added later on, so people can start writing cancellation-aware code today and it'll be optimized later on.
And of course, that's a _different_ model from abort semantics - which can be added anyway through a token-based approached (discussion on https://esdiscuss.org/topic/promises-as-cancelation-tokens )
Abort semantics are needed because browsers have limited amount of connections to the server, so if connections remain open after cancel() declares disinterest, and there could be situation when connection limit is reached
Consuming a promise is very explicit interest in its result.
@benjamingr that's certainly not always the case, e.g. when result is undefined for one. There's also lots of time-math, scheduling and utility functions one can do and make that build on promises without taking a direct interest in their result (e.g. Promise.all or the queue(() => {}) thing I reference in https://github.com/whatwg/fetch/issues/27#issuecomment-102500578 which is from real code I wrote) . But you didn't address my concern that there's no way for a user to ensure this behavior in other libraries, which makes this action-at-a-distance of the worst kind in my book: Users and library authors shouldn't have to worry about indirectly affecting client code like this.
This is cancellable promises thing is really hard to stomach. So let's say I make a request like this:
let resultPromise = fetch(url).then(parseResponse).then(retrieveResult);
I'm assuming I'd only need to call cancel() on resultPromise to actually cancel the request? But if I did this instead:
let responsePromise = fetch(url).then(parseResponse);
let resultPromise = responsePromise.then(retrieveResult);
I'm now assuming I would need to call cancel() on _both_ responsePromise and resultPromise? That's insanity, obviously, so I'm assuming you're suggesting that once responsePromise is garbage collected, it will intrinsically signal disinterest. Besides being non-deterministic, that might have made sense until you think about wanting to make a request where you don't care about the result, but still don't want it to cancelled:
fetch('/log/user_action');
What am I missing here?
@appden if you don't observe the result client-side, the browser is already allowed to terminate the connection. That is how all networking APIs behave.
@annevk that doesn't resonate with my experience with XMLHttpRequest. I don't think I've ever _needed_ to add a "load" event listener to ensure the request goes through. A quick search through the specification didn't show me anything to think otherwise. Would you mind linking me to a source that confirms what you're telling me?
https://xhr.spec.whatwg.org/#garbage-collection
@annevk interesting, thanks. That makes it crystal clear I was wrong about that. :smile:
But what about this:
let unusedPromise = fetch(url).then(handleResponse);
The unusedPromise would be garbage collected, but I'd still want handleResponse to be called when the request succeeds, so clearly garbage collecting the promise shouldn't cause the request to be cancelled.
However, by that logic I'd never be able to cancel this request...
let resultPromise = fetch(url).then(parseResponse).then(retrieveResult);
...without retaining references to all the intermediate promises and calling cancel() on them? The system can't really know if my parseResponse function is operating as a transformer on the response, or a handler that actually uses the response to, for instance, update the UI on my site.
When you can observe the response through script, the browser is indeed not allowed to terminate the fetch. How cancel() should work is unclear and not specified yet, so I don't know how to answer that question.
@appden
const responsePromise = fetch(url).then(parseResponse);
const resultPromise = responsePromise.then(retrieveResult);
responsePromise.cancel() would also result in the cancelation of resultPromise if responsePromise has not yet settled (meaning resultPromise has not yet settled).
resultPromise.cancel() would also result in the cancelation of responsePromise if resultPromise has not setted _and_ responsePromise has not settled.
const responsePromise = fetch(url).then(parseResponse);
const resultPromise = responsePromise.then(retrieveResult);
const anotherResultPromise = responsePromise.then(retrieveResultDifferently);
Here, canceling anotherResultPromise will only cancel anotherResultPromise. responsePromise is not canceled because resultPromise is interested in the result.
Some of that example code looks dubious. Wouldn't you have to do this? For a long-polling usecase:
if (currentFetch) { currentFetch.cancel(); }
const fetchPromise = fetch(...);
fetchPromise.then(parseResponse).then(processResult);
currentFetch = fetchPromise;
@riking Yes, you would. That's the problem with Promise semantics for this use case. The fetchPromise.then(parseResponse) call has the same effect as a fetchObservable.map(parseResponse).subscribe(). There is no way to tell a Promise you just want to map the value and that you don't care about the output yet, so you'd have to cancel all the intermediate Promises produced by the .then chain.
This is a major reason @blesh was advocating for Tasks. They're a type with the single-valued properties of a Promise, which is certainly more appropriate here, but with separate .map and .subscribe methods like an Observable so that you can support cancellation or disinterest trivially.
@riking I don't fully understand the aim of your code. I'd handle long-polling like this:
function poll(url, handleResult) {
return fetch(url)
.then(handleResult)
.catch(() => wait(1000))
.then(() => poll(url, handleResult));
}
var poller = poll(url, func);
// later...
poller.cancel();
@jakearchibald because the cancel() would be implemented on the promise-like returned by fetch() and not by the .then() method, right?
I assume that if a fetch is cancelled before it resolves, the catch() handlers would run.
.then returns a promise of the same type, so cancelablePromise.then() returns a cancelable promise.
@zowers
Abort semantics are needed because browsers have limited amount of connections to the server, so if connections remain open after cancel() declares disinterest, and there could be situation when connection limit is reached
Disinterest semantics are capable of dealing with this just fine as they do in other systems. Abort semantics are highly discouraged everywhere to the point a lot of higher level systems deprecated or removed them altogether from user-level code. Aborting a promise (or observable, or task, or whatever) parallels to a thread abort. It's very dangerous and very abusable. It's giving users a footgun.
@jan-ivar
There's also lots of time-math, scheduling and utility functions one can do and make that build on promises without taking a direct interest in their result (e.g. Promise.all or the queue(() => {})
Uhh.. consuming a promise is clearly a way to signal a direct interest in its results. This is a case where users would be bitten by not having sound disinterest semantics.
Your .catch(() => {}) example is a real use case though - typically in order to suppress unhandled rejections or to suppress errors explicitly. This of course can be dealt with either by calling .cancel on that promise immediately or creating a method that suppresses rejections without interfering with cancellation. In bluebird we call it suppressUnhandledRejections and it solves that semi-problematic use case you speak of explicitly.
I ask you to reconsider what semantics we'd like to give users - giving them cancellation with abort semantics is exactly the "action at a distance" you speak of. Giving them disinterest semantics covers every practical use case without letting them perform an action at a distance - all they can do is signal disinterest and the producer gets to do what they want with it.
@appden
I'm now assuming I would need to call cancel() on both responsePromise and resultPromise?
Nope, just because you created another reference to a promise does not mean you've created another promise. You would only have to cancel the very last promise and all intermediate results would propagate the disinterest. It's much easier to stomach than you'd imagine. I encourage you to experiment with bluebird and see how simple the semantics are for yourself.
...without retaining references to all the intermediate promises and calling cancel() on them? The system can't really know if my parseResponse function is operating as a transformer on the response, or a handler that actually uses the response to, for instance, update the UI on my site.
Note _explicit_ cancellation would still cancel it (and not execute the then handler), this is not the same as garbage collection. So calling .cancel would do what you'd expect it to.
@riking
Some of that example code looks dubious. Wouldn't you have to do this? For a long-polling usecase:
No, that code is "write once", you'd write a combinator for doing it once and lifting a promise returning function to only care about the last result (like Rx has flatMapLatest) and then call it whenever you want the result.
@eplawless
@riking Yes, you would. That's the problem with Promise semantics for this use case.
Sorry but from what I understand that's not the reason @blesh is advocating for tasks and observables here at all. Please don't put words in his mouth. The difference between promises and observables _for this use case_ is that promises proxy results and RxJS observables parallel functions and not their results. That's why you can write .flatMapLatest on an RxJS observable but with Promises or another value primitive you'd need to call flatMapLatest on the _function_ returning the promise.
See the related interesting discussion here. You can read more about multicast and unicast here and of course on https://github.com/kriskowal/gtor.
Anyway, you can read my response to @appden above. The only difference between promises and observables in this regard is that the most popular observable library ships with this built in and the most popular promise library (assuming: native promises) does not.
@appden
I assume that if a fetch is cancelled before it resolves, the catch() handlers would run.
No, that does not settle with disinterest semantics. Promise cancellation is like a generator's return called on the generator. It runs finally blocks but not catch handlers. Catch handlers running implies abort semantics which is really dangerous. Again, this is something I encourage you to experiment with with bluebird promises - I'm confident after a few tries you'll see just how intuitive and sound it is.
Uhh.. consuming a promise is clearly a way to signal a direct interest in its results.
@benjamingr My library has no inherent interest in absorbing responsibility over whether what came before should be cancellable or not. Arguably, control to prevent cancellation is in many instances as powerful as cancellation itself, yet the disinterest pattern appears to avoid these instances rather than solve them. Not great for my library which will have no control over who's calling it.
(quote from elsewhere) in bluebird semantics if you opt in to cancellation promise cancellation has disinterest semantics
I don't see anything opt-in about disinterest (an interest method would be opt-in). My library never opted in and still has to call it, it sounds, if it wants to avoid causing callers grief. If all it takes is for one person in a parade to call it a demonstration to make it a demonstration, then that's not opt-in.
Your .catch(() => {}) example is a real use case though - typically in order to suppress unhandled rejections or to suppress errors explicitly. This of course can be dealt with either by calling .cancel on that promise immediately.
Look closely, and it's done on an internal _fork_ of the promise chain, one that's never returned to the caller from this queue implementation. It exists to force separation between passed-in promise-returning functions which have nothing in common except only one is dealt with at a time (e.g. errors from one fork does not affect another, yet cancellation violates this separation?)
See https://github.com/whatwg/fetch/issues/27#issuecomment-87134228 for why this is harmful.
Other examples: For instance, here are two common patterns for injecting logging:
return p.catch(e => {
debug.log("This happened, fyi: " + e);
throw e;
});
or:
p.catch(e => debug.log("This happened, fyi: " + e));
return p;
Neither cause the promise chain to halt, but one of them causes cancellation to halt.
Lastly, for argument's sake, say I'm willing to call cancel in my implementation. When and how would I call it? Even after I've returned my promise, I may still be racing with some callers calling then on it, which I think means I risk accidentally cancelling the caller's previous operations immediately, right? Wouldn't that be a hard-to-find disaster?
Even if you have multiple branches, the branch that you cancel is cancelled regardless if others still want to see the result. If you are suggesting something else, namely that you cancelling your branch also affects others, that's never going to happen.
@benjamingr Thanks for the response. I'd read https://github.com/kriskowal/gtor before but reading it again is fairly illuminating:
Promises are broadcast.
The law that no consumer can interfere with another consumer makes it impossible for promises to abort work in progress. A promise represents a result, not the work leading to that result.
A task has mostly the same form and features as a promise, but is unicast by default and can be cancelled. A task can have only one subscriber, but can be explicitly forked to create a new task that depends on the same result. Each subscriber can unsubscribe, and if all subscribers have unsubscribed and no further subscribers can be introduced, a task can abort its work.
Tasks are unicast and therefore cancelable.
See the accompanying sketch of a task implementation.
I appreciate the correction, the important distinction then is multicast (like Promise) vs unicast (like Task or Stream). I was wrong that Task separates .map and .subscribe. The sketch implementation has separate .then (.map.subscribe) and .done (.subscribe) methods, only one of which is allowed to be called, and only once, on a given task.
So, it matters that map and subscribe are conflated for multicast values because multiple consumers can e.g. .then a Promise, leading to interference in the case where cancellation or disinterest is added. It doesn't matter if _unicast_ values have a .then method which does both, because calling it twice on the same Task (instead of calling .fork explicitly) is an error.
You mentioned Bluebird and I think that's a really interesting example. Bluebird v2 seems to have cancellation semantics, which I think we agree isn't the solution, while v3 has disinterest semantics. The documentation mentions the reference counting technique, which I think is the most viable way to have this work.
It still suffers from interference, though. What would you expect to happen here?
var promise = fetch(url);
promise.then(sub1);
promise.then(sub2);
promise.cancel();
Nope, just because you created another reference to a promise does not mean you've created another promise.
This is likely a nitpick but for the example @appden gave, based on the Promises/A+ spec, there will absolutely be a second Promise created if retreiveResult returns a value, will there not?
let responsePromise = fetch(url).then(parseResponse);
let resultPromise = responsePromise.then(retrieveResult);
It still suffers from interference, though. What would you expect to happen here?
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.
@jan-ivar
@benjamingr My library has no inherent interest in absorbing responsibility over whether what came before should be cancellable or not
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 here
On the other hand:
var p = cancellablePromiseFactory();
var p2 = p.then(...);
var p3 = p.then(...);
p2.cancel(); // refcounting semantics means it's unsound to abort here
Example: 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.
@eplawless
I appreciate the correction, the important distinction then is multicast (like Promise) vs unicast (like Task or Stream).
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 .publish().refCount() is so common that there are utility methods basically so you don't have to type it.
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 throw on a second subscriber.
That said, again, we have sound promise cancellation today - so while tasks are a very interesting concept to explore given the current state of fetch it sounds like a much easier path and one users will find a lot simpler.
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.
This is likely a nitpick but for the example @appden gave, based on the Promises/A+ spec, there will absolutely be a second Promise created if retreiveResult returns a value, will there not?
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.
See https://jsfiddle.net/v6LrLdsj/
var p = cancellablePromiseFactory(); p.catch(() => {}); p.cancel(); // p gets cancelled here
@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/
var p = cancellablePromiseFactory();
p.catch(() => {});
p.then(() => {}).cancel(); // callers cancel here
If this helps, we are currently using in production a fork of fetch() that adds an abort() method to the request object. It works as such:
// 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…
@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.
@WebReflection I take @davidbonnet 's point to be that we're over-designing and getting stuck: Have fetch return an object that acts a little like a promise, and no more (for now). Lets get something done.
@WebReflection Indeed, we morphed the polyfill into a module. Otherwise, as you mention, our custom fetch function wouldn't be available in modern browsers such as Chrome.
The point is to show an actual use case to help find a solution for this feature.
yes @jan-ivar the only thing needed was a special case/override of the returned object with a .cancel or .abort method because the problem was: "how to stop a request at all before the response is ready?"
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 .abort in has nothing to do with following the polyfill approach.
Best Regards
@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 request API that simply wraps the fetch one but it doesn't exposes the fetch object itself directly so that everyone that didn't care 'till now can keep being careless and those that need to eventually drop a request before it has a response available can.
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 fetch that points to the fetch result so that a polyfill would look like the following:
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
@jan-ivar
Here you're cancelling at the intersection point. Of course that works. The problem is downstream.
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 https://github.com/whatwg/fetch/issues/27#issuecomment-87134228 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 innocuousActivity more than once - cancellation wouldn't make sense anyway. A correct way to write it is:
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 fetch. Something like:
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 abort on the chain in the part that does strict side effects.
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 catch clauses run) and users would have to guard explicitly against that.
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.
@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 fetch imo. With sound cancellation semantics.
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.
@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 fetch(). I'm sure that work will be heavily influenced by this thread and bluebird.
That's not the use case you describe in the comment you linked to though - it fixes that.
@benjamingr It absolutely is, and it doesn't fix it. I must not have communicated well in https://github.com/whatwg/fetch/issues/27#issuecomment-176472760. 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 fooImpl to get cancelled, but it wont be with disinterest semantics.
Again, the real code is here (someCleanup = legacyCatch).
The owner of the promise is willfully making the promise uncancellable, working as intended
Fine:
let p = impossibleToIgnoreCancellableFunction()
.then(() => foo(3000))
.then(() => console.log("here"))
.catch(e => console.error(e));
p.cancel();
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. p is just the client here.
@petkaantonov Do you agree the above will _never_ cancel impossibleToIgnoreCancellableFunction?
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 p was the only client.
@petkaantonov I think this might help clear up some of the confusion. The other interpretation is that impossibleToIgnoreCancellableFunction() has 3 clients, one for each .then or .catch method, and that cancelling p might just cancel the .catch client.
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.
yep, understood. it's down to whether that's the desired semantic. I couldn't say one way or the other.
@petkaantonov Sounds like you get it will _never_ cancel impossibleToIgnoreCancellableFunction. Now assume, in a future version, I update foo to no longer use a chain internally. Suddenly it will _always_ cancel impossibleToIgnoreCancellableFunction. This is action at a distance at its most exquisite.
This is action at a distance at its most exquisite.
I don't understand @jan-ivar you changed the _thing at a distance_ and now it's behaving differently. That's not action at a distance at all and moreover the behavior _at the consumer side_ did not change at all and remained the same.
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.
Imagine foo is in some library written by someone else. This is exactly what action at a distance is.
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.
Imagine foo is in some library written by someone else. This is exactly what action at a distance is.
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 foo, that's not the situation with any other semantics for cancellation of fetch and half the point of disinterest semantics.
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.
As far as I understand no one is suggesting that though.
.
If someone changes the code at a distance to behave differently of course you get action-at-a-distance.
Assume that foo and cancellableFunction have nothing in common, except that they both use promises. foo's contract with the client is to return something unrelated at some point in the future. cancellableFunction on the other hand does something visible, like painting on the screen or blinking lights, that the user cares about, and cares about whether it is happening or not.
The client does not care exactly how foo is implemented - and shouldn't have to - and is therefore surprised when cancellableFunction does not stop blinking, and surprised again when it starts working later. _It is very difficult, however, to track down which component is responsible_. Is it because the client skipped calling foo, or updated to a newer version of foo? The only way to find out is trial and error!
Blaming the implementation of foo is low. foo could care less about cancellation and does not know or care about cancellableAction. When foo takes the _innocent action_ of changing whether to internally sequence multiple calls on itself - something that appreciably should have no effect whatsoever on clients whose calls don't overlap, which is the case here - then clients are suddenly affected in an unintended and surprising way. The _behavior in one part of a program varies wildly based on difficult or impossible to identify operations in another part of the program_.
Worse, you still haven't told me how foo can fix this so it doesn't interfere with cancellation.
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 foo
The client intended for p.cancel() to cancel cancellableFunction. When defined semantics don't match client intent, then the semantics suck.
While cancel / abort isn't wrong, most of the time (for me) the intent is actually free or deprecate. I want to free that network request to both not have the client handle irrelevant data (if possible), or stop the server from processing it (when its very costly to compute). Sometimes throttling user actions just isn't good enough.
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 deprecate methods can have options to let the user decide how liberal fetch can be with aborting and how it should handle the abort case. I don't believe its hard to implement most variations as options and by default its probably unsafe to abort once content has trickled down unless the user explicitly asks for it, or fetch can determine that the partial content streamed down is still only known to itself, so...
// 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 want
Deprecating a request would not guarantee an abort. An abort would simply be one possible outcome. If fetch can't determine either (a) its safe to abort, or (b) the user explicitly asked for the unsafe option, fetch should just not abort and that's that.
Ignoring the promise should work with current promise implementations just fine, but it would probably be ideal for Promise to support deprecate as well (ie. explicitly specify that the promise is now "garbage" and should neither resolve nor error out). While I would assume javascript engines would (or should) be able to figure out the promises in question and related handlers are garbage with out it being explicit, unhandled Promise checkers would probably have issues with them (though I believe only Promise libraries actually implement anything of the sort at the moment).
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.
@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.
@benjamingr awesome! :+1:
What is the status of this suggestion?
@pyryjook Cancelable Promise ES proposal will cover this.
@benjamingr You said you'd respond to my criticism of this idea in https://github.com/whatwg/fetch/issues/27#issuecomment-178657827, and even going back earlier to https://github.com/whatwg/fetch/issues/27#issuecomment-102500578, 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?
@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:
fetch(request, token)
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 cancel on a promise.
Also @jan-ivar this is all stage 1, I trust @domenic to take good care of fetch when approaching stage 2 and look into it.
the repository is called Cancelable promise but it's not actually about promise cancellation. It's about third state semantics.
That is not correct; it subsumes both. The es-cancel-token repo serves as inspiration but is not currently under consideration by the committee.
@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
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 .cancel() method to promises, then yes, that is not currently under consideration.
Thanks, then @jan-ivar please provide feedback in https://github.com/domenic/cancelable-promise :)
I left comments in https://github.com/domenic/cancelable-promise/issues/25 though it was closed as out of scope.
PTAL. No better place for general discussion it seems.
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.
@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 response.body.cancel().
@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?
@jeffbski if I understand correctly you can consider a fetch like an XHR split in 3 readyState cases: 0 creation, 2 headers received or request failed already, 4 end of the request.
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 .cancel() in the meanwhile it works, but I'd expect it to do so.
@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.
@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.
@jeffbski you basically discovered why this thread exists :D
in XHR the readyState 2 is when headers has been sent and are available to be read (aka: also received) while readyState 3 is the downloading process, what streams do with fetch.
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)
@Noiwex I switched from fetch to superagent. Suggest you do something similar until the fetch API is finalized
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. https://github.com/mzabriskie/axios/pull/452 I hasn't been deployed in a release yet, but should be in the next one since it is in master branch.
This is a known limitation of js. I hate this limitation.
function fit () {
....
// Somewhere in depth.
function ololo() {
console.log('ololo');
setTimeout(ololo, 1000);
}
ololo();
...
}
var _fit = new fit();
setTimeout(function () {
// Our single CPU is here.
// Listen, I don't want you to go into _fit anymore!
// Please remove all points that you've made in CPU scheduler for _fit call.
// _fit.kill();
// process.cpu_scheduler.remove_all_points_created_from(_fit);
// process.cpu_scheduler.points.forEach(function (point) {
// if (point.stack.some(function (parent) {
// return parent === _fit;
// }) {
// point.remove();
// }
// });
// myCustomCPUSchedulerForFit.points.forEach(function (point) {
// point.remove();
// });
// myCustomCPUSchedulerForFit.destroy();
}, 2000);
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?
function fit () {
var self = this;
...
if (!self.isAborted) {
...
}
var _fit = new fit();
setTimeout(function () {
_fit.isAborted = true;
...
}, 2000);
isAborted means that _fit call is dead. But our single CPU will dive into our dead scope again and again. We will receive performance and business logic issues just because we couldn't kill the dead scope.
You can see such bool guards in any javascript project. Javascript is just a toy. You shouldn't use it for any reliable solutions.
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
@WebReflection, This is not just about setTimeout, this is about any way for process to create a point in it's schedule.
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 setTimeout, but you couldn't remove setimmediate and nextTick even in your own scope. You couldn't really cancel your promise. You could just simulate this behaviour.
Javascript have to obtain a schedule management api. It's thread model is not completed. Otherwise any js developer will create isAborted, isDestroyed workarounds forever.
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:
fetch(request, cancelToken)
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.
The cancellable promises proposal has been withdrawn just now -- what now?
@SEAPUNK Any specific reason why? (I don't follow the proposal discussion)
@syndbg
tc39/proposal-cancelable-promises#70
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.
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).
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 resolve or reject logic .
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.
@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.
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.
Really unsatisfying this whole thread. So much information and no outcome ..
@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")?
I think this is all we need (requires Chrome or Firefox Developer Edition at the moment).
@jan-ivar can you please link to the spec of that :)?
@benjamingr It's based on this discussion.
Closing in favour of https://github.com/whatwg/fetch/issues/447 as this thread's getting a little long, and contains a lot of discussion of cancelable promises that are unfortunately no longer relevant.
how to cancel a fetch request (promise)
https://developer.mozilla.org/en-US/docs/Web/API/AbortController
https://caniuse.com/?search=AbortController
https://dom.spec.whatwg.org/#aborting-ongoing-activities
Most helpful comment
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 Promise
I 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
resolveandreject: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
abort(..)on it, and that affects my internal observation of the promise, then the promise has lost all of its trustability as an immutable value.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.
Controller
If 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:
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
pCancelas well.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:
Now, you can share the
promisearound and it's read-only immutable and observable, and you can separately decide who gets the control capabilities. For example, I'd send onlypromiseto some external consumer, but I might very well retain thepCancelinternally for some usage.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 ==
asyncCancelIn addition to what I've observed about how promise cancelation should be designed, I don't think we should let the cancelation of a promise mean "abort the fetch". That's back-pressure, and there are other more appropriate ways to model that than promise cancelation.
In fact, it seems to me the _only_ reason you would want to do so is merely for the convenience of having the fetch API return promises. Mere convenience should be way down the priority list of viable arguments for a certain design.
I would observe that the concern of what to do with aborting fetches is quite symmetric with the concern of how/if to make an ES7
asyncfunction cancelable.In that thread, I suggested that an
asyncfunction should return an object (ahem, controller) rather than a promise itself.To do promise chaining from an async function call in that way, it's only slightly less graceful. The same would be true for a fetch API returning a controller.
But if you want to access and retain/use the control capabilities for the async function (like signaling it to early return/cancel, just as generators can be), the controller object would look like:
I also drew up this crappy quick draft of a diagram for a cancelable async function via this controller concept:
That's basically identical to what I'm suggesting we should do with fetch.
PS: Is it mere coincidence that canceling a fetch and canceling an
asyncfunction both ended up issue number 27 in their respective repos? I think surely not! :)