Skip to content
New issue

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

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

Already on GitHub? Sign in to your account

Returning a FetchPromise from fetch() #625

Closed
annevk opened this issue Feb 16, 2015 · 50 comments
Closed

Returning a FetchPromise from fetch() #625

annevk opened this issue Feb 16, 2015 · 50 comments

Comments

@annevk
Copy link
Member

annevk commented Feb 16, 2015

We want to expose these features over time and the best candidate is the Request object:

  1. Messaging with the service worker's FetchEvent.
  2. Aborting a fetch.
  3. Modifying a fetch on the go (e.g. changing priority).

I think this requires that we change Request that after passing it to fetch() it is no longer usable. And change fetch() to not copy the object, but instead mutate it.
#624 is related to this.

Please let me know what you think about this soonish so I start making the required changes.

@annevk
Copy link
Member Author

annevk commented Feb 16, 2015

One weird thing is that this means that e.g. fetch(string) would never have those features. An alternative way of exposing these features would be to keep some kind of registry per environment that tracks all the fetches. This is something the Web Performance WG is already doing per APIs designed by @igrigorik, but not really with any of this in mind I think.

@wanderview
Copy link
Member

From my thoughts on IRC the other day:

I personally think Request is an awkward place to put stateful information and mutating methods like this. These only really apply if you've passed the Request ro fetch(), but we are using Request for other APIs like Cache. Also, it makes clone() kind of a footgun since you should in theory lose the stateful extras when that is called.

It would really be nice to have some kind of object representing the in-progress fetch operation. It seems we don't have a good way to return this from fetch() directly, though.

The registry approach seems like it might be ok.

Or maybe we could have fetch() callback with a control object. Something like:

function onFetchStart(fetchControl) {
  fetchControl.abort();
}

fetch('/my/url.html', { onstart: onFetchStart }).then(function (response) {
  // normal stuff
});

Just not very promise-like, though.

@jakearchibald
Copy link
Contributor

If the only difference this introduces is:

var r = new Request('/');
fetch(r);
fetch(r);

…making the 2nd fetch fail, then this is fully compatible with how Chrome already behaves.

Modifying a fetch on the go (e.g. changing priority).

+1 - I've already seen people expect adding headers to fetchEvent.request to work.

Aborting a fetch.

What's the API proposal here? I thought it'd be a cancellable promise + cancellable stream? Neither of which seem to need changes to the current model (and would work with fetch(str))

@jakearchibald
Copy link
Contributor

@wanderview the right API fit feels like extendable promises

var fetchPromise = fetch('/');

// as usual
fetchPromise.then();

// but also
fetchPromise.abort();

But I understand extendable promises haven't been worked out.

@wanderview
Copy link
Member

I think we discussed on IRC that the extendable promise approach doesn't work as a place to put things like changePriority() or port.postMessage().

Some other ideas:

fetch('my/url.html', { withControl: true }).then(function (fetchControl) {
  // fetchControl.abort()
  // fetchControl.changePriority()
  // fetchControl.post.postMessage()
  return fetchControl.response();
}).then(function (response) {
  // normal stuff
});

Or perhaps some kind of transforming function that takes a fetch returned Promise and gives you a Promise to the fetchControl.

withControl(fetch('my/url.html')).then(function (fetchControl) {
  // fetchControl.abort()
  // fetchControl.changePriority()
  // fetchControl.post.postMessage()
  return fetchControl.response();
}).then(function (response) {
  // normal stuff
});

@annevk
Copy link
Member Author

annevk commented Feb 16, 2015

@jakearchibald subclassing promises and having declarative syntax for promises is not really compatible.

@annevk
Copy link
Member Author

annevk commented Feb 16, 2015

I've already seen people expect adding headers to fetchEvent.request to work.

That is a different case from modifying a request that is already in the network layer. It's an interesting case as well, but very different.

@annevk
Copy link
Member Author

annevk commented Feb 16, 2015

If fetch(req) no longer clones, fetch(req, options) needs to mutate req. This would then be different from new Request(req, options), which creates a copy by definition. This would be an observable change.

(We should probably also fix the definition of new Request(req, options) to not mess up req if the operation ends up throwing. Would be nice if failure didn't result in side effects.)

@annevk
Copy link
Member Author

annevk commented Feb 16, 2015

I tried to summarize our options here: https://gist.github.com/annevk/56e97115f2c9c2e90c23

@domenic
Copy link
Contributor

domenic commented Feb 16, 2015

I think subclassing promises should be done with care but isn't necessarily incompatible with async/await. That is, you can do

const inProgressFetch = fetch(req);

// Do other stuff, possibly async...
const otherInfo = await someOtherStuff();

if (otherInfo.shouldCancel) {
  inProgressFetch.cancel();
  return;
}

const response = await inProgressFetch;

That said, I think it would require some concerted all-hands-on-deck API design to come up with the correct semantics for cancelable promises in the timeframe we seem to be working on, here.

I also wanted to put out there as an option the following syntax:

const inProgressFetch = fetch.controllable(req);

Maybe in this example inProgressFetch is @wanderview's { abort(), changePriority(), port, ready }.

Finally regarding the names "abort" vs. "cancel," in streams I came up with (fairly artificially) the idea that abort is "forceful" and will cause the resulting stream to error, as seen by any other consumers of it, whereas cancel signals "loss of interest" and will just act as if the stream is closed. In promises I think the semantics might be rejecting with a new Cancellation exception for "abort" vs. becoming forever-pending (and cleaning up underlying resources) for "cancel." Amusingly, both of these semantics are being tried in promise libraries these days.

@annevk
Copy link
Member Author

annevk commented Feb 17, 2015

Yeah, @jakearchibald convinced me that subclassing is fine even with async/await. Especially since you often want to store the promise in a variable anyway to do concurrent processing.

Note that I think that if we expose something like abort() here, it should also affect the eventual stream. So perhaps we should just go with a term used in fetching, "terminate", and give it the semantics of terminating the connection (either rejecting the promise or canceling the stream).

@annevk annevk changed the title Changing Request and fetch() Returning a FetchPromise from fetch() Feb 17, 2015
@jakearchibald
Copy link
Contributor

Although having methods on the promise that function even after the promise has settled feels a bit odd, I agree it's the practical thing to do here.

@annevk
Copy link
Member Author

annevk commented Feb 19, 2015

Filed https://www.w3.org/Bugs/Public/show_bug.cgi?id=28057 for IDL support.

@jakearchibald
Copy link
Contributor

@domenic I'm trying to figure out how this works, but I find the ES6 spec heavy going. Can you clear up a few things?

Firstly, is this how it should work?

var p = fetch(url);
p.abort();
// aborts fetch for url

var p = fetch(url).then(r => r.json());
p.abort();
// aborts fetch for url and rejects promise so .json is never called
// or if fetch already resolved:
//   terminate the stream and therefore reject .json()

var p = fetch(url1).then(_ => fetch(url2));
p.abort();
// aborts fetch for url1 and rejects promise so url2 is never fetched
// or if fetch(url1) already resolved:
//   terminate the stream for url1
//   abort the fetch for url2

var p = fetch(url1).then(_ => wait(3000)).then(_ => fetch(url2));
p.abort();
// aborts fetch for url1 and rejects
// or if fetch(url1) already resolved:
//   terminate the stream for url1
//   reject the 'wait' so url2 never fetches
// or if wait(3000) already resolved:
//   terminate the stream for url1
//   abort the fetch for url2

var p = Promise.resolve(fetch(url));
p.abort();
// undefined is not a function (fetchPromise has been casted to a normal promise)

If the above is right, we need a generic AbortablePromise, then a hook on that for specific abort steps which FetchPromise would extend.

var p = fetchPromise.then(_ => "Hello");

For the above to work, .then() needs to add state (or an onabort callback) to the new promise to maintain the link back to the original fetch. This cannot be done with @@species since that's on the this object's constructor. If @@species was checked on the this object, it could maintain state that way.

I imagine I'm missing a simpler way.

@jakearchibald
Copy link
Contributor

From @tabatkins

var fetchPromise = fetch(url);
var jsonPromise = fetchPromise.then(r => r.clone().json());
var textPromise = fetchPromise.then(r => r.text());

textPromise.abort(); // should this cause jsonPromise to abort?

@wanderview
Copy link
Member

From a stream point of view, I think closing (or aborting) one stream should not close a peer stream. I think this should also be true for Response clones.

So I would vote for not aborting the jsonPromise in this case.

@tabatkins
Copy link
Member

It obviously shouldn't. ^_^

This is an ambient authority bug, caused by the abort capability being carried along by default to all the chained promises. You really do have to be strict about whether an object is multicast or unicast; if you work with a unicast object in a multicast way, it needs to act multicast, and not expose dangerous capabilities.

The proper way to handle this is:

var fetchTask = fetch(url);
var jsonTask = fetchTask.pipe(r => r.clone().json());
var textTask = fetchTask.pipe(r => r.text());

textTask.abort(); // aborts the textTask operation, but leaves fetchTask alone
jsonTask.abort(); // now that all consumers are aborted, fetchTask can abort too

In other words, you need to ref-count your clients with abort capabilities if you want to be able to propagate aborts up the chain. You don't want to give that out by default; it'll just mean that aborts rarely propagate. You need to be able to both pass out the ability to observe the result (a normal Promise) and the higher-powered ability to manipulate the fetch, and these need to be separate operations.

Alternately, you could just use standard .then() for this behavior, and require people to do Promise.resolve(fetchTask) when they want to hand out the result of the fetch without increasing the refcount (both giving out the ability to abort the fetch, and making it harder for other clients to abort the fetch). But let's be honest, that won't actually happen in most code. I'm not sure how much we want to value the different ergonomics.

@domenic
Copy link
Contributor

domenic commented Feb 20, 2015

var p = fetch(url).then(r => r.json());
p.abort();
// aborts fetch for url and rejects promise so .json is never called
// or if fetch already resolved:
//   terminate the stream and therefore reject .json()

We were also contemplating leaving the promise forever-pending instead of rejecting it, if the fetch is in progress.

var p = fetch(url1).then(_ => fetch(url2));
p.abort();
// aborts fetch for url1 and rejects promise so url2 is never fetched
// or if fetch(url1) already resolved:
//   terminate the stream for url1
//   abort the fetch for url2

The latter is not entirely clear. We could design it either way---return values are ignored, or return value abortion is supported.

Analogously would be the question:

var p = FetchPromise.resolve(otherFetchPromise).abort();

Does abortion of p imply abortion of otherFetchPromise?

var p = fetch(url1).then(_ => wait(3000)).then(_ => fetch(url2));
p.abort();
// aborts fetch for url1 and rejects
// or if fetch(url1) already resolved:
//   terminate the stream for url1
//   reject the 'wait' so url2 never fetches

Well, only if wait also returns a FetchPromise (or other abort-able promise). You can't just reject arbitrary promises.

// or if wait(3000) already resolved:
//   terminate the stream for url1
//   abort the fetch for url2

This comes down to the same question as above.

var p = Promise.resolve(fetch(url));
p.abort();
// undefined is not a function (fetchPromise has been casted to a normal promise)

Yes, definitely.

If the above is right, we need a generic AbortablePromise, then a hook on that for specific abortion steps which FetchPromise would extend.

That's one approach. If we want that, someone's going to have to do the design for a generic AbortablePromise, which is going to be tricky. See e.g. last paragraph of https://esdiscuss.org/topic/aborting-an-async-function#content-9.

For the above to work, .then() needs to add state (or an onabort callback) to the new promise to maintain the link back to the original fetch.

Sure, just define then appropriately. For example, here's one design: promises-aplus/cancellation-spec#6; replace this.constructor with this.constructor[Symbol.species] since that code was written before species existed.

// should this cause jsonPromise to abort?

This is the biggest issue with cancellable promises, IMO. Our current take is to make then fail when called twice on a single cancellable promise. That way you can use it like a normal promise in most cases (including await), but you cannot extract its value twice.

@tabatkins
Copy link
Member

This is the biggest issue with cancellable promises, IMO. Our current take is to make then fail when called twice on a single cancellable promise. That way you can use it like a normal promise in most cases (including await), but you cannot extract its value twice.

Uuuuuuggggghhhh, that's not a Promise anymore, then. Please don't screw with the interface like this; it'll break things in confusing ways, especially as more tooling shows up around Promises.

If you want .then() to only be callable once, you don't have a Promise. You have a Task. If that's what we want, great, but then let's make it much clearer that this isn't a promise; call the chaining method .pipe(), etc.

@domenic
Copy link
Contributor

domenic commented Feb 21, 2015

It's worth mentioning an alternate approach which I think avoids the ambient authority problems. @bterlson brought it up when I talked to him about this. It is the cancellation token idea.

A cancellation token basically has three things: requestCancel(), isCancelled, and setCancelHandler(). The consumer is meant to create one, give it to the producer, and later call requestCancel(). requestCancel() will set isCancelled to true and execute any callback set by setCancelHandler(). Then the producer can periodically check isCancelled at break points during their work, or call setCancelHandler() to get a callback executed when requestCancel() is called. So for fetch this would probably look something like

const token = new CancellationToken();
fetch("http://example.com", token);

// later
token.requestCancel();

There's a slight modification to this design that re-uses the promise mechanism. E.g. if CancellationToken is actually { requestCancel(), cancelled } where cancelled is a promise and requestCancel() is the promise's corresponding resolve function, then things are basically equivalent, but with less moving parts and no possibility for synchronous cancellation (which is probably fine?).

@domenic
Copy link
Contributor

domenic commented Feb 21, 2015

If you want .then() to only be callable once, you don't have a Promise. You have a Task. If that's what we want, great, but then let's make it much clearer that this isn't a promise; call the chaining method .pipe(), etc.

Then you can't use it with await or promise combinators or anything else. It's almost certainly not worth creating an entire parallel universe of "tasks" and forcing people to choose between promises and tasks every time they want to do something async.

@tabatkins
Copy link
Member

You don't need to create an alternate universe. I outlined a good option earlier in IRC: Tasks can be Promises too, they just create a plain Promise when you call .then()/etc. If you operate on it like it's a multicast object, it'll behave like one; you have a different API (.pipe(), which is otherwise identical to .then()) for when you want to preserve the dangerous parts and treat it like unicast. (You can fiddle with details there; .pipe() could ref-count and be callable multiple times, or maybe it's single-call and you have to clone explicitly; whatever.)

(I haven't read your previous post yet; feedback probably incoming.)

@domenic
Copy link
Contributor

domenic commented Feb 21, 2015

So you still can't await them and then cancel the result, or use any library that expects a promise and then cancel the result (including well-coded promise combinators that use e.g. p.constructor), in your version. Thus, parallel universe, which has to use .pipe instead of .then.

@tabatkins
Copy link
Member

Promise combinators aren't going to preserve aborting either; you can mix multiple types of Promises! A specialized CancelablePromise combinator can know how to clone/pipe, so no special work required there.

await can potentially know about the difference between Promises and Tasks, and clone/pipe appropriately.

There's nothing parallel-universe about this, except that if you explicitly treat them as multi-cast, you either lose aborting, or you leak capabilities. This is inescapable, and you already ran into it with streams, so I'm unsure of why you're resisting here.

@domenic
Copy link
Contributor

domenic commented Feb 21, 2015

My point is that people writing libraries don't want to think "oh, I'll need to write another library for people who are using tasks instead of promises." They'll just use then. And now you can't use their library.

With the then-only-works-once approach, all those libraries can be used just fine, as long as they don't assume multicast (which very few do).

@tabatkins
Copy link
Member

That's how this works, yes. Either you leak ambient authority, or naive libraries will lose authority. Then-only-works-once just means that any library which chains off of a promise twice (which is totally fine for every other promise in the entire world) will break when you hand it one of these special not-really-a-promise objects. Or if you pass the promise to two libraries, each of which chains once.

It means you can't, for example, use the promise in two combinators. Or do a "totally safe" Promise.resolve() on the value to assimilate arbitrary thenables for your library. Or a bunch of other things.

Then-only-works-once means it's not a Promise any more, period, and it's not a good idea. :(

There aren't that many shortcuts around GTOR.

@domenic
Copy link
Contributor

domenic commented Feb 21, 2015

I guess all I can say is that I disagree.

@tabatkins
Copy link
Member

Edit: NM, that's chaining two levels, which isn't multicast.

@tabatkins
Copy link
Member

The other way to do it that doesn't break perfectly normal behavior is to have .then() still return a FetchPromise (abortable), but you can call .then() multiple times and it ref-counts - if all of its chains abort, it aborts as well.

Then you can cast to a plain Promise with Promise.resolve(fetchp), like normal. (Or, since that'll leak a ref, maybe we do add an explicit .promise() or something to it that returns a plain promise that just observes the value. That makes it easy to prevent leaking a ref to libraries that'll chain off of the value without returning the chained object, too.)

Really, the reason I strongly disagree here is that observing the value multiple times is not a problem. That's totally fine; the problem is that always returning a FetchPromise when you chain leaks the abort capability as well. You're solving the capabiliity-leak by removing the ability to observe multiple values, which is pretty scorched-earth. There are more delicate ways to solve this that have similar usability.

@NekR
Copy link

NekR commented Feb 25, 2015

Consider please this example:

var a  = fetch(...);
var b = fetch(...);
var c = fetch(...);

var req = Promise.all([a, b, c]);

req.then(function() {
  // handle load somehow
});

onEscPressed(function() {
  req.abort();
});

Here is 3 request, for example, made one one user action. Let's say it's dialog. I as a developer sometimes want to controls such request as a one. So for this case I would to use Promise.all to wait all request or to abort all in one when dialog becomes closed. So, here is the questions:

  • Will Promise.all case those promises to generic Promise? If so, what thing developers should use, FetchPromise.all?
  • Will req.abort() cancel all fetch promises? What will happen if one of them will be generic-non-cancelable promise?

@domenic
Copy link
Contributor

domenic commented Feb 25, 2015

Will Promise.all case those promises to generic Promise? If so, what thing developers should use, FetchPromise.all?

Yes and yes.

Will req.abort() cancel all fetch promises?

That would be a design decision that we have to make when designing FetchPromise.all

What will happen if one of them will be generic-non-cancelable promise?

It will be converted to a FetchPromise using FetchPromise.resolve, which presumably gives it a no-op cancellation action.

@tabatkins
Copy link
Member

Agreed on all counts.

@NekR
Copy link

NekR commented Feb 25, 2015

That would be a design decision that we have to make when designing FetchPromise.all

I strongly believe what in that situation it should cancel all promises. At least because [FetchPromise|Promise].all is special case. I mean special relatively to .then().

@Jxck
Copy link

Jxck commented Feb 26, 2015

I'm wondering fetch() as only a single function is enough capable for extending with new feature
which we currently not find out.

I think fetch() is a process fetching for Request and create Response.
Request / Response is basically a container of request / response data result of fetching.

so if we will get new feature in process of fetching (like HTTP3, 4, 5...?)
how we can add new api to fetch?
adding a callback for that in option arg?
extend a returned promise as SomeFeatureAddedFechPromise ?

my opinion is simply.
how about add Fetch class?

if we have a fetch instance instead of function,
we have choice to add method to that for new capability in future.
in abort case, we simple add abort() method to Fetch class.

if that is builtin class, developer also can extend that for their own class.
I think it's seems more extensible web thing for me.

@annevk
Copy link
Member Author

annevk commented Feb 26, 2015

Isn't that essentially what FetchPromise would be?

@jakearchibald
Copy link
Contributor

I like @tabatkins' ref-count idea. Although I'd only count child abortable promises as refs, Promise.resolve(fetchp) would be a simple observer.

So

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 rejects (or hangs) as a result

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

@jakearchibald
Copy link
Contributor

@Jxck

I think it's better to stay Promise as generic for asynchronous processing

Aborting is a common & desirable feature of asynchronous processing

@domenic
Copy link
Contributor

domenic commented Feb 26, 2015

@Jxck if there's some feature of fetching that requires a different signature than Request -> FetchPromise<Response>, then it should be a different function, not called fetch, since that's the basic processing model for fetch. There's no reason to put them in a class; this isn't Java.

@tabatkins
Copy link
Member

Although I'd only count child abortable promises as refs, Promise.resolve(fetchp) would be a simple observer.

Yeah, that's the entire point, actually; you can cast it to a standard Promise to strip it of its additional powers, so of course you don't need to ref-count it.

@otakustay
Copy link

I have a question:

var p = waitForUserConfirm('press OK to continue').then(() => fetch(url));

Since a generic Promise#then would "cast" a FetchPromise to a generic Promise, p will not have the abort method, in this case how can I abort the fetch?

For this, I like the CancellationToken approach more since it gives control wherever a fetch sits in an async operation chain

var token = new CancellationToken();
var p = waitForUserConfirm('press OK to continue').then(() => fetch(url, token));
app.on('navigate', token.requestCancel); // possible to cancel fetch when navigate to another page

@tabatkins
Copy link
Member

@otakustay Yeah, I'm not sure how to address that scenario either, with the FetchPromise approach. Darn.

I guess looking seriously into token approaches is the way to go; that separates the capability from the class, so you don't have to do gymnastics to keep the type correct and the cancellation chain going.

We'll need to think about ergonomics, though; particularly, how to offer the same "chain rejections upward" scenarios we talked about previously. The rejection-chaining worked well when every chained FetchPromise was refcounted by its parent, and if all the current children got cancelled the parent got cancelled. How can we reproduce this with a cancellation token? It would probably be annoying to have to create new cancellation tokens for each chained promise. Maybe you really do only get the ability to abort the original "capable of being cancelled" action, and it's up to you to manage consumers wanting to to cancel? This seems unfortunate, and I'd like to think on it.

@NekR
Copy link

NekR commented Mar 1, 2015

I am not sure if you disccused this before, but here is another thing which might work:

var req = fetch('...', function(abort) {
  onESCPressed(abort);
});

req.then(function() { ... }).catch(function(err) {
  // err -> FetchError, type: 'cancelation'
});

Something like that. I looks like Promise executor (function(resolve, reject) {}), but only provides abort capability (or maybe all others too?) and abort action is just a wrapper for reject action. Hence FetchPromise just rejects with cancelation reason.

@tabatkins
Copy link
Member

Ugh, I just realized that, pending some clever insight, the token approach doesn't work with combinators, unless you explicitly pass in the tokens as well, or have duplicate token combinators.

@NekR From what others have said, it appears preferable for a cancelled fetch (and cancelled things in general) to instead just be forever-pending; in real usage, rejecting just means that everybody puts a check at the top of their reject handler that does nothing if it was a cancel-rejection.

@NekR
Copy link

NekR commented Mar 1, 2015

@tabatkins

in real usage, rejecting just means that everybody puts a check at the top of their reject handler that does nothing if it was a cancel-rejection.

Yes, I know that and 100% with you. For example, jQuery has same approach for their abort action and it's really annoying. But I am not sure if forever-pending is better approach. Ability to know if promise/request is canceled (from other part of code) is a really usable thing.
Cancelable Promises seems absolutely better for this, altough I do not know their current state.
I can imagine Cancelable Promises as this:

var req = new Promise(function(fullfill, reject, cancel) {
  // ...
});

req
  .then(function onFullfill() {}, function onReject() {}, function onCancel() {})
  .catch(function onReject() {})
  .cancelled(function onCancel() {});

This version is backwards-compatible, it has no problems with casting and for old-Promises (current, non-cancelable) cancelation will be just forever-pending state (+this version is polyfillable).

P.S. cancelled() is just example and might be any other word which better fits

@tabatkins
Copy link
Member

This is just pulling the "cancellation" ability into the core Promise. That's possible, but I don't think we want to go that way unless absolutely necessary.

@otakustay Reviewing your example from earlier, I see I missed a possibility. Here's the example again:

var p = waitForUserConfirm('press OK to continue').then(() => fetch(url));

You correctly pointed out that this'll cast the FetchPromise back into a normal Promise, which is unfortunate, and I thought that was a killer objection. But it's not! It's not the most ergonomic, but you can get around this by just converting the first Promise into a FetchPromise first:

var p = FetchPromise.resolve(waitForUserConfirm('press OK to continue')).then(() => fetch(url));

This upgrades your original Promise into a FetchPromise with no-op cancellation behavior. With this, the .then() properly assimilates the return value of fetch(), maintaining the FetchPromise-ness, so you can still cancel it.

So we can continue looking at type-based solutions rather than token-based ones, which makes me happy because they have much better ergonomics.

(The one problem, looking forward, is that they don't compose; you can't really make a promise that is both a FetchPromise and a FooPromise, for some interesting new value of Foo. This seems identical to the problem of mixing monads; you just need to write the equivalent of a monad transformer to handle combining capabilities.)

@tolmasky
Copy link

tolmasky commented Mar 2, 2015

@otakustay perhaps this is really out there, but in C#/Unity land when I was using their task/await system (not promise based), I was able to (IMO) elegantly handle situations like this by structuring the code as such (pseudo-code):

while (true)
    page = await Q.race(navigate(), page.actions())

Apologies since the explanation is a bit cyclic (assume we have "cancelation" to explain how cancelation works). But basically, the idea is given high level primitives that know what to do with cancellation (race which returns the value of the first promise that finishes first and cancels all the other running promises, all which waits for all the promises to finish, etc), then you can describe these relationships in the actual structure of the code vs passing variables around.

So here, navigate would behave as such:

function navigate()
{
    return new Promise(function(resolve, reject)
    {
        app.on("navigate", resolve);
    });
}

So in other words, navigate never finishes unless the user navigates. When it does navigate, it returns the respective "page" object for said page, which has its own little world of action represented by the actions method. The actions method probably simply never returns on its own (simply waits to get canceled by the navigation). So, no matter what the page is doing, a navigation action necessarily cancels everything, returns a new page, and kicks of the process with the next page's actions.

With this kind of structure you kind of design your code flow like a flow diagram, with everything thats necessarily coupled at the top.

@otakustay
Copy link

@tabatkins

I'm afraid if in your code

var p = FetchPromise.resolve(waitForUserConfirm('press OK to continue')).then(() => fetch(url));

the p.abort() can really abort the fetch process, which is a basic requirement for us, how can a FetchPromise.resolve create a FetchPromise which is aware of the fetch call?

We don't need a no-op abort method, we need one which actually aborts the fetch

@tabatkins
Copy link
Member

I'm imagining that the resolving behavior for FetchPromise creates a no-op cancel when the argument is not a FetchPromise, but when it is, it properly sets up a chaining cancel. So the FetchPromise created by .then() resolves with the result of fetch, and chains its cancel from it. As long as nothing else chains off of the fetch() result, the ref-count will be 1, so when you call p.cancel() it'll chain up and cancel the fetch() result as well.

(Sorry, I thought this was obvious; otherwise chaining cancellable things doesn't work at all, in any circumstance.)

@annevk
Copy link
Member Author

annevk commented Mar 19, 2015

Please note that this discussion is now also taking place in #592 (comment)

@annevk
Copy link
Member Author

annevk commented Mar 26, 2015

Closing this in favor of whatwg/fetch#27

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

9 participants