- From: Kyle Simpson <notifications@github.com>
- Date: Thu, 26 Mar 2015 12:49:52 -0700
- To: whatwg/fetch <fetch@noreply.github.com>
- Message-ID: <whatwg/fetch/issues/27/86688768@github.com>
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` and `reject`: ```js 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 tenant in not only software design (avoid "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: ```js 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: ```js 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. ## Abort != Promise Cancelation... Abort == `async` Cancel In 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](https://github.com/lukehoban/ecmascript-asyncawait/issues/27). In that thread, [I suggested](https://github.com/lukehoban/ecmascript-asyncawait/issues/27#issuecomment-74175526) 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. ```js 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: ```js 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: <a href="https://github.com/lukehoban/ecmascript-asyncawait/issues/27#issuecomment-78140651"><img src="https://cloud.githubusercontent.com/assets/150330/6584461/880ef514-c739-11e4-8a82-302312adc695.png" width=200></a> That's basically identical to what I'm suggesting we should do with fetch. --- Reply to this email directly or view it on GitHub: https://github.com/whatwg/fetch/issues/27#issuecomment-86688768
Received on Thursday, 26 March 2015 19:50:21 UTC