Description
Goal
Provide developers with a method to abort something initiated with fetch()
in a way that is not overly complicated.
Previous discussion
- Add timeout option #20
- Cancelling HTTP fetch w3c/ServiceWorker#592
- Returning a FetchPromise from fetch() w3c/ServiceWorker#625
- Getting errored/closed reader, Auto-releasing reader, Forcible cancel() on a stream streams#297
- Potential security issues when applied to fetch() tc39/proposal-cancelable-promises#4 (security issues)
Viable solutions
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.
A promise-subclass
In order to not clash with cancelable promises (if they ever materialize) we should pick a somewhat unique method name for abortion. I think terminate()
would fit that bill.
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.
A controller
The limited discussion on es-discuss https://esdiscuss.org/topic/cancelable-promises seemed to favor a controller. There are two flavors that keep coming back. Upfront construction:
var c = new FetchController
fetch(url, {controller: c})
c.abort()
Revealing constructor pattern:
fetch(url, {controller: c => c.abort()})
Open issues
- What is the effect on the promise? Both forever-pending and explicit rejection have reasonable arguments. We could offer the choice to the developer, but what should be the default?
- What is the effect on the stream? I suspect the Streams Standard is already conclusive on this.
- What syntax of the above two-three solutions do we favor?
Activity
WebReflection commentedon Mar 26, 2015
Thanks for the effort folks, I'm chiming in to follow up and with a couple of questions:
abort
andcancel
and you think thatterminate
would pay the naming bill. Wouldn't be wise to use similar XHR intent developers already know instead of introducingterminate
for the fetch andabort
for the controller?Best Regards
annevk commentedon Mar 26, 2015
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 eithercancel()
and/orabort()
(as mentioned btw).jakearchibald commentedon Mar 26, 2015
The controller approach is certainly the quickest way we'll solve this, but it's pretty ugly, I'd like to treat it as a last resort & try for the cancellable promises approach.
Cancellation based on ref-counting
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.
Cancelling a promise that hadn't already settled would cancel all its child CancellablePromises.
Observing cancellation
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:
Usage in fetch
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:
jakearchibald commentedon Mar 26, 2015
Clearing up a question from IRC:
In the above,
fetchPromise.abort()
does nothing as the promise has already settled. The correct way to write this would be:Now
jsonPromise.abort()
would cancel either the request, or the response, whichever is in progress.mariusGundersen commentedon Mar 26, 2015
Since calling abort might not abort the fetch (request), I don't think the method should be called
abort
(orcancel
, as it is in some other specificatins), but ratherignore
, since that is all it can guarantee to do. For example,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 callingignore
will always ignore the result. For example:jakearchibald commentedon Mar 26, 2015
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:
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 pickedabort
because of XHR, maybecancel
is a better fit.jyasskin commentedon Mar 26, 2015
@jakearchibald, in your proposal, it looks like
p.then()
increments the refcount, butPromise.resolve(p)
doesn't? And that promises start out with a 0 refcount, so that you don't have to alsoabort()
the initialfetch()
? 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.
jakearchibald commentedon Mar 26, 2015
@jyasskin the refcount would be increased by cancellable promises. So
cancellablePromise.then()
increases the refcount, as wouldCancellablePromise.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.Yeah, it should
onCancel
.jyasskin commentedon Mar 26, 2015
'k. Say we have a careless or cancellation-ignorant library author who writes:
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: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
orCancellationToken
if the goal here is to get something quickly instead of waiting for the long-term plan to emerge.martinthomson commentedon Mar 26, 2015
Another alternative:
That would make the activity on fetch dependent on a previous promise in a similar fashion (but more intimate) to this:
I don't like the implicit nature of what @jakearchibald suggests here.
getify commentedon Mar 26, 2015
TL;DR
I would like to speak strongly in favor of the "controller" approach and strongly opposed to some notion of a cancelable promise (at least externally so).
Also, I believe it's a mistake to consider the cancelation of a promise as a kind of automatic "back pressure" to signal to the promise vendor that it should stop doing what it was trying to do. There are plenty of established notions for that kind of signal, but cancelable promises is the worst of all possible options.
Cancelable 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
resolve
andreject
: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
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:
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 onlypromise
to some external consumer, but I might very well retain thepCancel
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.
Abort != Promise Cancelation... Abort ==
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.
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
async
function both ended up issue number 27 in their respective repos? I think surely not! :)jhusain commentedon Mar 26, 2015
Related: Composition Function Proposal for ES2016
Might be interested in the toy (but instructive) definition of Task and how it is composed using async/await.
https://github.com/jhusain/compositional-functions
372 remaining items