Aborting a fetch #27

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

Comments

Projects
None yet
Owner

annevk commented Mar 26, 2015 edited by domenic

Goal

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

Previous discussion

Viable solutions

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

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

Owner

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

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

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

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

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.

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

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

azu referenced this issue in azu/meta-fetch-issue Mar 27, 2015

Open

Fetch API issue tracking #1

Owner

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.

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?

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

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

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

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

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

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

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.

getify commented Mar 27, 2015

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

Collaborator

jakearchibald commented Mar 27, 2015

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

Collaborator

jakearchibald commented Mar 27, 2015

@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 slightlyoff/ServiceWorker#362 (comment)). 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.

getify commented Mar 27, 2015

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

Collaborator

jakearchibald commented Mar 27, 2015

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

NekR commented Mar 27, 2015

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

Collaborator

jakearchibald commented Mar 27, 2015

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

Collaborator

jakearchibald commented Mar 27, 2015

That would mean fetch(url).then(r => r.json()) isn't abortable once the stream starts being read into json.

NekR commented Mar 27, 2015

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.

Collaborator

jakearchibald commented Mar 27, 2015

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?

Collaborator

yutakahirano commented Mar 28, 2015

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

Collaborator

jakearchibald commented Mar 28, 2015

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

NekR commented Mar 28, 2015

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

Collaborator

jakearchibald commented Mar 30, 2015

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

Collaborator

jakearchibald commented Mar 30, 2015

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

Collaborator

jakearchibald commented Mar 30, 2015

@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

Collaborator

jakearchibald commented Mar 30, 2015

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

Collaborator

jakearchibald commented Mar 30, 2015

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

Collaborator

jakearchibald commented Mar 30, 2015

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.

Collaborator

jakearchibald commented Mar 30, 2015

@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" notifications@github.com
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
#27 (comment).

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" notifications@github.com wrote:

@WebReflection https://github.com/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.


Reply to this email directly or view it on GitHub
#27 (comment).

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.

getify commented Mar 30, 2015

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

getify commented Mar 30, 2015

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

getify commented Mar 30, 2015

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.

jhusain commented Mar 30, 2015

It is precisely because of the limitations of promises that there is currently a proposal to replace async/await with a more extensible syntax that can apply to other types such as as .NET-style Task. Providing syntactic support for promises encourages people to use them where they are inappropriate. Fetch is an excellent example of an API that should not use Promises, because it involves the use of a scarce resource (ie connections).

Promises are a very well articulated concept. They are the asynchronous equivalent of a value returned by a synchronous function. When you call a function synchronously it cannot be canceled. Rather than try and evolve a Promise into something else, or inappropriately use it in order to get better language support, why not invent a new type that is just as compositional as a promise, but has the required semantics for fetch?

A reference-counted Task provides the necessary semantics for fetch. Rather than providing a guarantee of cancellation, you can simply give the producer the ability to determine whether anyone is listening for the outcome of an asynchronous operation. If all consumers stop listening for the outcome, the producer can have the opportunity to cancel the operation because it is not observable. In my opinion this is the global maximum, because consumers do not have to concern themselves with cancellation errors. The request can only be canceled if there are no listeners, which means that when the request is finally canceled, no one is around to hear it. Reference counting also ensures that consumers do not need to be made aware of other consumers.

This approach works very well for Netflix which often uses ref-counted scalar Observables of 1 to represent async operations. We would not use a Promise for asynchronous requests in the browser because there are too many UI interactions that require rapid creation and cancellation of pending data requests (autocomplete box being the most common).

JH

On Mar 30, 2015, at 4:56 PM, Kyle Simpson notifications@github.com 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.

getify commented Mar 30, 2015

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.

jhusain commented Mar 30, 2015

This is a very reasonable concern in principle, but in my experience is not a problem in practice. Perhaps this is a matter of not clearly defining what I mean by producer and consumer. In this context by producer I mean "fetch" which controls the code which backs the Task. By consumer I mean the code that assigns a callback to a Task and receives a subscription which can be used to stop listening for the result:

var task = fetch(...);

var subscription = task.get(value => console.log(value), error => console.error(error));

// subscription.dispose() can be used to stop listening

In this context the producer (fetch) does not cancel unless there are no more consumers. Fetch doesn't have sufficient context to cancel a request. As for garbage collection, disposing of the subscription removes the reference from the task to the handlers passed to get. This breaks the link and allows GC to occur.

Can you clarify what you mean by producer? All application use cases I have encountered in UIs can be accommodated with the notion of consumer unsubscription. An event may occur which may cause a consumer to stop listening for the result of a task, such as a form being closed. In these situations it can be the consumers responsibility to explicitly unsubscribe from a Task when events occur which cause their eventual data to become irrelevant. This can even be done declaratively with compositional functions as in the example below:

var task = fetch(...);

var subscription =
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 notifications@github.com 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:

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

getify commented Mar 30, 2015

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

jhusain commented Mar 30, 2015

I believe it is much easier (and more desirable) to add a version of fetch that returns a Task than it is to change the definition of Promise.

I'm afraid I've been unclear about what I mean when I say reference counting. What I really mean is subscription counting. Every task can keep a simple counter of the number of subscriptions that exist. This mechanism is totally outside of the garbage collector and implemented in plain old JavaScript. Because it is necessary for each consumer to explicitly unsubscribe, there is a clear opportunity to decrement the counter.

The Task I propose would have a "then" method which would have the same signature as a promise. However it would also be lazy and also have a get method which was used to actually retrieve the data. "Then" would not trigger the request, just create a new Task. The get method would be used to actually retrieve the data. You can see an example (minus the reference counting) here:

https://github.com/jhusain/compositional-functions/blob/master/README.md

Now subscription counting is simply a matter of counting the number of get calls in the system, not the number of references to the Task. This is how the Observables in Rx work, using flatMap for Compositon and forEach for consumption.

JH

On Mar 30, 2015, at 5:45 PM, Andrea Giammarchi notifications@github.com 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 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?


Reply to this email directly or view it on GitHub.

getify commented Mar 30, 2015

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.

Collaborator

jakearchibald commented Mar 30, 2015

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.

getify commented Mar 30, 2015

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?

getify commented Mar 30, 2015

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

getify commented Mar 30, 2015

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.

getify commented Mar 30, 2015

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" notifications@github.com 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
#27 (comment).

getify commented Mar 30, 2015

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.

NekR commented Mar 30, 2015

It's really hard to follow all the things here by now. But can we all agree at least of basic concept things before actual implementation details?

  • 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.
  • As fetch(...).cancel() is chainable then fetch also should have some grouping operators for it. fetch.all(...) or CancelablePromise.all(...) does not really matter now.
  • All those stuff should absolutely fine works with 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.

getify commented Mar 30, 2015

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

NekR commented Mar 30, 2015

Just to be clear @getify, I am not talking here about "concepts" in general, but rather about "implementation concepts" and not "implementation details of those concepts". Main goal of this is to stop flying around few differing concepts arguing with implementations details, many heuristics, etc. We need to choose something already here, otherwise it will never end.

So, let's me comment your items, but please, do not go into "I believe this is concept, but this is not". Let's agree on things which easy to decide (some high-level concept over upcoming implementation of them).

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.

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.

jan-ivar commented Feb 1, 2016

@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 #27 (comment) but that's obviously not a very good example since bluebird semantics would work there. A bigger issue is that it contains incorrect code and has an inherent race condition for caching a flag and not a promise, more than one invocation will call 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 :)

Owner

annevk commented Feb 2, 2016

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.

jan-ivar commented Feb 2, 2016

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 #27 (comment). Let me try again:

let operations = Promise.resolve();

function chain(func) {
  let p = operations.then(() => func());
  operations = p.catch(() => {});
  return p;
};

let someCleanup = () => { /* doesn't matter what */ };
let fooImpl = x => new Promise(resolve => setTimeout(resolve, x));

let foo = x => chain(() => fooImpl(x)).then(() => someCleanup());

let p = foo(3000).then(() => console.log("here")).catch(e => console.error(e));
p.cancel();

Here one would expect 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

jan-ivar commented Feb 2, 2016

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.

jan-ivar commented Feb 2, 2016

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

jan-ivar commented Feb 2, 2016

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

jan-ivar commented Feb 2, 2016

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.

.

jan-ivar commented Feb 3, 2016

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! 👍

What is the status of this suggestion?

@benjamingr You said you'd respond to my criticism of this idea in #27 (comment), and even going back earlier to #27 (comment), but that was 4 months ago, so I assumed this terrible idea was dead. I was apparently wrong. Have you had time to ponder what I'm not seeing?

mislav referenced this issue in github/fetch Jun 21, 2016

Closed

[Q] timeout handling #175

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

Owner

domenic commented Jun 21, 2016

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?

Owner

domenic commented Jun 21, 2016 edited

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?

Owner

domenic commented Jun 21, 2016

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 domenic/cancelable-promise#25 though it was closed as out of scope.

PTAL. No better place for general discussion it seems.

@sleepycat sleepycat added a commit to sleepycat/mapbox-gl-geocoder that referenced this issue Jun 28, 2016

@sleepycat sleepycat Replace request with isomorphic-fetch
This change started it's life as a bughunt.

While debugging the error `A wildcard '*' cannot be used in the 'Access-Control-Allow-Origin'
header when the credentials flag is true` I came across
request/request#986. markbahnman's comment in that issue resolves that
particular error which then revealed another request related error
`TypeError: self.req.abort is not a function`.

Given the characterization of request as "a Node.js library,
used server-side." and the errors, it seemed an opportune moment to
explore removing request and exploring another library.

This commit does exactly that, removing `request` and replacing it with
`isomorphic-fetch`.

One open question with this approach is that the old code would cancel
this.request if it existed. Cancelling a fetch is under discussion in
whatwg/fetch#27 is as yet unresolved.

Apart from that, the tests all pass and the lint is clean.
b14f008

Any update on this problem?

Collaborator

jakearchibald commented Jul 18, 2016

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.

@ghost

ghost commented Aug 12, 2016

@jakearchibald is there any to use this currently on a browser? Maybe through some kind of polyfill?

Collaborator

jakearchibald commented Aug 12, 2016

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

WebReflection commented Aug 18, 2016 edited

@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 commented Oct 9, 2016

So, guys, you seem to have some pretty discussion here, but when we're going to see a solution?

@ghost

ghost commented Oct 9, 2016 edited by ghost

@Noiwex I switched from fetch to superagent. Suggest you do something similar until the fetch API is finalized

Noiwex commented Oct 10, 2016

@lgdexter Yeah, my friend suggested me to use superagent instead too.

If you are only using in the browser, then can use rxjs ajax which supports cancellation.

Also in addition to superagent, axios just got support for cancellable promises based on the stage 1 proposal. mzabriskie/axios#452 I hasn't been deployed in a release yet, but should be in the next one since it is in master branch.

andrew-aladev commented Oct 11, 2016 edited

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.

WebReflection commented Oct 11, 2016 edited

The solution is to better use timers and address them, same as you would address listeners or you won't be able to remove them later on.

The concept of an ID, nothing new to learn.

function Fit() {
  // since you define at runtime functions
  function ololo() {
    console.log('ololo');
    return setTimeout(ololo, 1000);
  }
  // you can define at runtime methods too
  this.kill = function () {
    clearTimeout(interval);
  };
  // and invoke .kill() whenever you want
  var interval = ololo();
}

var fit = new Fit();
setTimeout(fit.kill, 500);

JS has been historically considered a toy probably because many developers stopped learning it, convinced they mastered it already, after reading just one book.

Moreover, the unaddressed timer issue has absolutely nothing to do with this problem, which is the same in many other programming languages where promises cannot be canceled.

A timer, like a listener, can always be removed, if you keep the reference somehow.

Best Regards

andrew-aladev commented Oct 11, 2016 edited

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

Noiwex commented Nov 3, 2016 edited

It just shows that fetch specification is really underdone.

SEAPUNK commented Dec 15, 2016

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

syndbg commented Dec 15, 2016

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

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 commented Dec 15, 2016 edited

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

WebReflection commented Dec 16, 2016 edited

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.

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.

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

Collaborator

jakearchibald commented Dec 16, 2016

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

pimterry referenced this issue in resin-io-modules/resin-register-device Dec 16, 2016

Closed

Migrate from `requests` to `fetch` #27

kevart commented Jan 4, 2017

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

stuartpb commented Jan 4, 2017

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

jan-ivar commented Jan 4, 2017

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

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

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