New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Aborting a fetch #27

Closed
annevk opened this Issue Mar 26, 2015 · 328 comments

Comments

@annevk
Member

annevk commented Mar 26, 2015

Goal

Provide developers with a method to abort something initiated with fetch() in a way that is not overly complicated.

Previous discussion

Viable solutions

We have two contenders. Either fetch() returns an object that is more than a promise going forward 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?
@WebReflection

This comment has been minimized.

Show comment
Hide comment
@WebReflection

WebReflection Mar 26, 2015

Thanks for the effort folks, I'm chiming in to follow up and with a couple of questions:

  • wouldn't be wise to put this resolution on hold until there is a definitive take on Promise-land and cancel-ability?
  • in this post you named 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

WebReflection commented Mar 26, 2015

Thanks for the effort folks, I'm chiming in to follow up and with a couple of questions:

  • wouldn't be wise to put this resolution on hold until there is a definitive take on Promise-land and cancel-ability?
  • in this post you named 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

@annevk

This comment has been minimized.

Show comment
Hide comment
@annevk

annevk Mar 26, 2015

Member

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).

Member

annevk commented 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 either cancel() and/or abort() (as mentioned btw).

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 26, 2015

Collaborator

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.

var rootFetchP = fetch(url).then(r => r.json());

var childFetchP1 = rootFetchP.then(data => fetch(data[0]));
var childFetchP2 = rootFetchP.then(data => fetch(data[1]));
var childP = Promise.resolve(rootFetchP).then(r => r.text());

childFetchP1.abort();
// …aborts fetch(data[0]), or waits until it hits that point in the chain, then aborts.
// fetch(url) continues

childFetchP2.abort();
// …aborts fetch(data[1]), or waits until it hits that point in the chain, then aborts.
// fetch(url) aborts also, if not already complete. Out of refs.
// childP hangs as a result

rootFetchP.then(data => console.log(data));
// …would hang because the fetch has aborted (unless it completed before abortion)

Cancelling a promise that hadn't already settled would cancel all its child CancellablePromises.

Observing 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:

var cancellablePromise = new CancellablePromise(function(resolve, reject) {
  // Business as usual
}, {
  onCancel() {
    // Called when this promise is explicitly cancelled,
    // or when all child cancellable promises are cancelled,
    // or when the parent promise is cancelled.
  }
});

// as a shortcut:
CancellablePromise.resolve().then(onResolve, onReject, onCancel)
// …attaches the onCancel callback to the returned promise

// maybe also:
cancellablePromise.onCancel(func);
// as a shortcut for .then(undefined, undefined, func)

Usage in 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:

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() });
});
Collaborator

jakearchibald commented 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.

var rootFetchP = fetch(url).then(r => r.json());

var childFetchP1 = rootFetchP.then(data => fetch(data[0]));
var childFetchP2 = rootFetchP.then(data => fetch(data[1]));
var childP = Promise.resolve(rootFetchP).then(r => r.text());

childFetchP1.abort();
// …aborts fetch(data[0]), or waits until it hits that point in the chain, then aborts.
// fetch(url) continues

childFetchP2.abort();
// …aborts fetch(data[1]), or waits until it hits that point in the chain, then aborts.
// fetch(url) aborts also, if not already complete. Out of refs.
// childP hangs as a result

rootFetchP.then(data => console.log(data));
// …would hang because the fetch has aborted (unless it completed before abortion)

Cancelling a promise that hadn't already settled would cancel all its child CancellablePromises.

Observing 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:

var cancellablePromise = new CancellablePromise(function(resolve, reject) {
  // Business as usual
}, {
  onCancel() {
    // Called when this promise is explicitly cancelled,
    // or when all child cancellable promises are cancelled,
    // or when the parent promise is cancelled.
  }
});

// as a shortcut:
CancellablePromise.resolve().then(onResolve, onReject, onCancel)
// …attaches the onCancel callback to the returned promise

// maybe also:
cancellablePromise.onCancel(func);
// as a shortcut for .then(undefined, undefined, func)

Usage in 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:

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() });
});
@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 26, 2015

Collaborator

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.

Collaborator

jakearchibald commented Mar 26, 2015

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.

@mariusGundersen

This comment has been minimized.

Show comment
Hide comment
@mariusGundersen

mariusGundersen Mar 26, 2015

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()

mariusGundersen commented Mar 26, 2015

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()
@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 26, 2015

Collaborator

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.

Collaborator

jakearchibald commented Mar 26, 2015

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.

@jyasskin

This comment has been minimized.

Show comment
Hide comment
@jyasskin

jyasskin Mar 26, 2015

Contributor

@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.

Contributor

jyasskin commented Mar 26, 2015

@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.

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 26, 2015

Collaborator

@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.

Collaborator

jakearchibald commented Mar 26, 2015

@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.

@jyasskin

This comment has been minimized.

Show comment
Hide comment
@jyasskin

jyasskin Mar 26, 2015

Contributor

'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.

Contributor

jyasskin commented Mar 26, 2015

'k. Say we have a careless or cancellation-ignorant library author who writes:

function myTransform(yourPromise) {
  return yourPromise
    .then(value => transform(value))
    .then(value => transform2(value));

If we had 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.

@martinthomson

This comment has been minimized.

Show comment
Hide comment
@martinthomson

martinthomson Mar 26, 2015

Contributor

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.

Contributor

martinthomson commented Mar 26, 2015

Another alternative:

let abortFetch;
let p = new Promise((resolve, reject) => { abortFetch = reject; });
fetch(url, { abort: p }).then(success, failure);
// on error
abortFetch();

That would make the activity on fetch dependent on a previous promise in a similar fashion (but more intimate) to this:

promiseYieldingThing().then(result => fetch(url)).then(success, failure);

I don't like the implicit nature of what @jakearchibald suggests here.

@getify

This comment has been minimized.

Show comment
Hide comment
@getify

getify 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 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.

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:

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.

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.

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! :)

getify commented Mar 26, 2015

TL;DR

I would like to speak strongly in favor of the "controller" approach and strongly opposed to some notion of a cancelable promise (at least externally so).

Also, I believe it's a mistake to consider the cancelation of a promise as a kind of automatic "back pressure" to signal to the promise vendor that it should stop doing what it was trying to do. There are plenty of established notions for that kind of signal, but cancelable promises is the worst of all possible options.

Cancelable 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:

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.

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:

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.

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.

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! :)

@jhusain

This comment has been minimized.

Show comment
Hide comment
@jhusain

jhusain 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

jhusain commented Mar 26, 2015

Related: Composition Function Proposal for ES2016

Might be interested in the toy (but instructive) definition of Task and how it is composed using async/await.

https://github.com/jhusain/compositional-functions

@annevk

This comment has been minimized.

Show comment
Hide comment
@annevk

annevk Mar 27, 2015

Member

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.

Member

annevk commented Mar 27, 2015

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.

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 27, 2015

Collaborator

@jyasskin

function myTransform(yourPromise) {
  return yourPromise
    .then(value => transform(value))
    .then(value => transform2(value));
}

myTransform(fetch(url)).cancel();

This would:

  1. Cancel the promise returned by .then(value => transform2(value))
  2. Cancel the promise returned by .then(value => transform(value)) because all its child promises cancelled
  3. Cancel yourPromise (which is fetch(url)) because all its child promises cancelled

This works as expected right?

Collaborator

jakearchibald commented Mar 27, 2015

@jyasskin

function myTransform(yourPromise) {
  return yourPromise
    .then(value => transform(value))
    .then(value => transform2(value));
}

myTransform(fetch(url)).cancel();

This would:

  1. Cancel the promise returned by .then(value => transform2(value))
  2. Cancel the promise returned by .then(value => transform(value)) because all its child promises cancelled
  3. Cancel yourPromise (which is fetch(url)) because all its child promises cancelled

This works as expected right?

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 27, 2015

Collaborator

@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.

Collaborator

jakearchibald commented Mar 27, 2015

@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.

@NekR

This comment has been minimized.

Show comment
Hide comment
@NekR

NekR Mar 27, 2015

@jakearchibald what is wrong with this way?

var req = fetch('...');

// or req.headers.then(...)
req.response.then(function(response) {
  if (response.headers.get('Content-Type') !== 'aplication/json') {
    req.cancel();
  }

  return response.json();
});

req.addEventListener('cancel', function() {
  // ...
});

// or Streams-like style
// closed/cancelled/aborted
req.closed.then(function() {
  // ...
});

Here fetch will return some FetchObject, not Promise itself.

NekR commented Mar 27, 2015

@jakearchibald what is wrong with this way?

var req = fetch('...');

// or req.headers.then(...)
req.response.then(function(response) {
  if (response.headers.get('Content-Type') !== 'aplication/json') {
    req.cancel();
  }

  return response.json();
});

req.addEventListener('cancel', function() {
  // ...
});

// or Streams-like style
// closed/cancelled/aborted
req.closed.then(function() {
  // ...
});

Here fetch will return some FetchObject, not Promise itself.

@getify

This comment has been minimized.

Show comment
Hide comment
@getify

getify Mar 27, 2015

@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.

getify commented Mar 27, 2015

@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.

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 27, 2015

Collaborator

@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();
Collaborator

jakearchibald commented Mar 27, 2015

@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();
@WebReflection

This comment has been minimized.

Show comment
Hide comment
@WebReflection

WebReflection Mar 27, 2015

answering @getify from my POV:

var parent = new Promise(function (res, rej, cancel) {
    var cancelable = setTimeout(res,100);
    cancel(function () {
      clearTimeout(cancelable);
    });
  }),
  child1 = parent.then(function(){ console.log("child1"); }),
  child2 = parent.then(function(){ console.log("child2"); });

Since you expose cancel-ability, you setup what should happen when you cancel so that 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 ;-)

WebReflection commented Mar 27, 2015

answering @getify from my POV:

var parent = new Promise(function (res, rej, cancel) {
    var cancelable = setTimeout(res,100);
    cancel(function () {
      clearTimeout(cancelable);
    });
  }),
  child1 = parent.then(function(){ console.log("child1"); }),
  child2 = parent.then(function(){ console.log("child2"); });

Since you expose cancel-ability, you setup what should happen when you cancel so that 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 ;-)

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 27, 2015

Collaborator

@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 😄. 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.

Collaborator

jakearchibald commented Mar 27, 2015

@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 😄. 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.

@getify

This comment has been minimized.

Show comment
Hide comment
@getify

getify Mar 27, 2015

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.

getify commented Mar 27, 2015

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.

@jyasskin

This comment has been minimized.

Show comment
Hide comment
@jyasskin

jyasskin Mar 27, 2015

Contributor

@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?

Contributor

jyasskin commented Mar 27, 2015

@jakearchibald Oh right, refcount-starts-at-0 again. I think you're right.

I think this all leads to the guideline that, if you have a 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?

@NekR

This comment has been minimized.

Show comment
Hide comment
@NekR

NekR Mar 27, 2015

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.

NekR commented Mar 27, 2015

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.

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 27, 2015

Collaborator

@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));
}
Collaborator

jakearchibald commented Mar 27, 2015

@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));
}
@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Mar 27, 2015

Collaborator

@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.

Collaborator

jakearchibald commented Mar 27, 2015

@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.

@martinthomson

This comment has been minimized.

Show comment
Hide comment
@martinthomson

martinthomson Mar 27, 2015

Contributor

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?

Contributor

martinthomson commented Mar 27, 2015

I haven't been convinced by this thread that we need to solve the general problem of transitive promise cancellation. That adds a lot of reference for a feature that will be rarely used.

It's a poor analogy, but other multithreaded promise-like things don't have a generic cancellation. Read the c# documentation on the topic, for example.

What is wrong with adding a simpler hook, solely for fetch use?

@martinthomson

This comment has been minimized.

Show comment
Hide comment
@martinthomson

martinthomson Mar 27, 2015

Contributor

Sorry, reference should have been complexity. I blame my phone keyboard.

Contributor

martinthomson commented Mar 27, 2015

Sorry, reference should have been complexity. I blame my phone keyboard.

@benjamingr

This comment has been minimized.

Show comment
Hide comment
@benjamingr

benjamingr Oct 11, 2016

Contributor

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.

Contributor

benjamingr commented Oct 11, 2016

I think we should lock this thread. It's attracting a lot of low-level comments and discussion of how fetch should be aborted happened already and as far as I know we'll have:

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.

@Noiwex

This comment has been minimized.

Show comment
Hide comment
@Noiwex

Noiwex Nov 3, 2016

It just shows that fetch specification is really underdone.

Noiwex commented Nov 3, 2016

It just shows that fetch specification is really underdone.

@SEAPUNK

This comment has been minimized.

Show comment
Hide comment
@SEAPUNK

SEAPUNK Dec 15, 2016

The cancellable promises proposal has been withdrawn just now -- what now?

SEAPUNK commented Dec 15, 2016

The cancellable promises proposal has been withdrawn just now -- what now?

@syndbg

This comment has been minimized.

Show comment
Hide comment
@syndbg

syndbg Dec 15, 2016

@SEAPUNK Any specific reason why? (I don't follow the proposal discussion)

syndbg commented Dec 15, 2016

@SEAPUNK Any specific reason why? (I don't follow the proposal discussion)

@SEAPUNK

This comment has been minimized.

Show comment
Hide comment

SEAPUNK commented Dec 15, 2016

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Dec 15, 2016

Collaborator

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.

Collaborator

jakearchibald commented Dec 15, 2016

Seems like things are up in the air right now. If there's no progress by January I think we should provide a way to reject the promise and cancel the stream.

We have tokens or a request method as options.

@stuartpb

This comment has been minimized.

Show comment
Hide comment
@stuartpb

stuartpb Dec 15, 2016

I like the "revealing constructor" for a "fetch controller" approach: that lets this be orthogonal to any designs intending to create a generalized structure for cancelling promises, by focusing on a fetch-specific API. This "Fetch Controller" object could be extended down the line to add other fetch-adjusting functionality like, say, reducing the priority of a fetch (in the sense of HTTP/2, where different resource downloads can have different priorities, IIRC).

stuartpb commented Dec 15, 2016

I like the "revealing constructor" for a "fetch controller" approach: that lets this be orthogonal to any designs intending to create a generalized structure for cancelling promises, by focusing on a fetch-specific API. This "Fetch Controller" object could be extended down the line to add other fetch-adjusting functionality like, say, reducing the priority of a fetch (in the sense of HTTP/2, where different resource downloads can have different priorities, IIRC).

@WebReflection

This comment has been minimized.

Show comment
Hide comment
@WebReflection

WebReflection Dec 16, 2016

I wish we could've kept this as simple as possible, like in 20 lines of logic fix, and eventually iterate later on:

// Native chainability fix
(p => {
  const patch = (orig) => function () {
    const p = orig.apply(this, arguments);
    if (this.hasOwnProperty('cancel')) {
      p.cancel = this.cancel;
    }
    return p;
  };
  p.then = patch(p.then);
  p.catch = patch(p.catch);
})(Promise.prototype);

// Cancelable Promise fix
Promise = (P => function Promise(f) {
  let cancel, p = new P((res, rej) => {
    cancel = (why) => rej({message: why});
    f(res, rej);
  });
  p.cancel = cancel;
  return p;
})(Promise);

Above snippet would've made this possible, without exposing 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.

WebReflection commented Dec 16, 2016

I wish we could've kept this as simple as possible, like in 20 lines of logic fix, and eventually iterate later on:

// Native chainability fix
(p => {
  const patch = (orig) => function () {
    const p = orig.apply(this, arguments);
    if (this.hasOwnProperty('cancel')) {
      p.cancel = this.cancel;
    }
    return p;
  };
  p.then = patch(p.then);
  p.catch = patch(p.catch);
})(Promise.prototype);

// Cancelable Promise fix
Promise = (P => function Promise(f) {
  let cancel, p = new P((res, rej) => {
    cancel = (why) => rej({message: why});
    f(res, rej);
  });
  p.cancel = cancel;
  return p;
})(Promise);

Above snippet would've made this possible, without exposing 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.

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Dec 16, 2016

Collaborator

@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.

Collaborator

jakearchibald commented Dec 16, 2016

@stuartpb that's another option, yeah. The overall question is should the method of aborting a request also abort the response, or are they two separate things. If they're separate, there should be some way to join them.

@stuartpb

This comment has been minimized.

Show comment
Hide comment
@stuartpb

stuartpb Dec 16, 2016

I don't see any reason a FetchController shouldn't control both, and, furthermore, allow introspection into the relationship between the two (ie. a method to query the state of the fetch, like XHR's readyState, check the progress of a response, stuff like that).

stuartpb commented Dec 16, 2016

I don't see any reason a FetchController shouldn't control both, and, furthermore, allow introspection into the relationship between the two (ie. a method to query the state of the fetch, like XHR's readyState, check the progress of a response, stuff like that).

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Dec 16, 2016

Collaborator

@stuartpb this could help with upload progress too. Interesting.

Collaborator

jakearchibald commented Dec 16, 2016

@stuartpb this could help with upload progress too. Interesting.

@kevart

This comment has been minimized.

Show comment
Hide comment
@kevart

kevart Jan 4, 2017

Really unsatisfying this whole thread. So much information and no outcome ..

kevart commented Jan 4, 2017

Really unsatisfying this whole thread. So much information and no outcome ..

@stuartpb

This comment has been minimized.

Show comment
Hide comment
@stuartpb

stuartpb Jan 4, 2017

@jakearchibald What can I do if I want to help the spec move forward with the "revealing fetch controller constructor" design (as there appears to have been "no progress by January")?

stuartpb commented Jan 4, 2017

@jakearchibald What can I do if I want to help the spec move forward with the "revealing fetch controller constructor" design (as there appears to have been "no progress by January")?

@jan-ivar

This comment has been minimized.

Show comment
Hide comment
@jan-ivar

jan-ivar Jan 4, 2017

I think this is all we need (requires Chrome or Firefox Developer Edition at the moment).

jan-ivar commented Jan 4, 2017

I think this is all we need (requires Chrome or Firefox Developer Edition at the moment).

@benjamingr

This comment has been minimized.

Show comment
Hide comment
@benjamingr

benjamingr Jan 4, 2017

Contributor

@jan-ivar can you please link to the spec of that :)?

Contributor

benjamingr commented Jan 4, 2017

@jan-ivar can you please link to the spec of that :)?

@jan-ivar

This comment has been minimized.

Show comment
Hide comment
@jan-ivar

jan-ivar commented Jan 4, 2017

@benjamingr It's based on this discussion.

@jakearchibald

This comment has been minimized.

Show comment
Hide comment
@jakearchibald

jakearchibald Jan 4, 2017

Collaborator

Closing in favour of #447 as this thread's getting a little long, and contains a lot of discussion of cancelable promises that are unfortunately no longer relevant.

Collaborator

jakearchibald commented Jan 4, 2017

Closing in favour of #447 as this thread's getting a little long, and contains a lot of discussion of cancelable promises that are unfortunately no longer relevant.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment