tc39 / proposal-cancelable-promises

Former home of the now-withdrawn cancelable promises proposal for JavaScript
Other
376 stars 29 forks source link

Allow auto-cancelation inside an async function at every await point #23

Closed domenic closed 8 years ago

domenic commented 8 years ago

I am extracting this from #19, which in general is about an entirely different proposal unrelated to this repo (and thus closed), but inspired an idea which could be useful for this repo.

The idea is to have syntax sugar for automatically inserting cancelToken.cancelIfRequested() after every await point inside an async function. Doing this implicitly has gotten pushback in the past, but doing it explicitly seems to make sense to me. The strawman is then that we would allow

async function pollForValue(bus, targetValue, cancelToken) {
  await.cancelToken = cancelToken;

  while (true) {
    const answer = await bus.read();

    if (answer === targetValue) {
      return;
    }

    await delay(1000, cancelToken);
  }
}

as sugar for

async function pollForValue(bus, targetValue, cancelToken) {
  while (true) {
    const answer = await bus.read();
    cancelToken.cancelIfRequested();

    if (answer === targetValue) {
      return;
    }

    await delay(1000, cancelToken);
    cancelToken.cancelIfRequested();
  }
}

(using a slightly-tweaked version of the motivating example for cancelIfRequested, see #16).

This may be similar to what @inikulin was asking for in #18.

/cc @wycats since I believe this might address some of his concerns.

bergus commented 8 years ago

Two questions:

domenic commented 8 years ago

Did you forget the Promise.race() again or do you really only want to cancel the async function after the read or delay have settled?

Promise.race seems irrelevant.

You can only cancel after the read or delay is settled anyway. Before that there's no way for code outside the function run since the async function itself is running.

What does .cancelIfRequested() return? A cancelled promise? Or does it not return but complete with an abrupt cancellation? (Do these things cross function boundaries now?)

Please read the spec... more like the latter.

bergus commented 8 years ago

You can only cancel after the read or delay is settled anyway. Before that there's no way for code outside the function run since the async function itself is running.

I'm not sure what you mean. Let me explain with an example:

async function demo(cancelToken) {
  await.cancelToken = cancelToken;
  try {
    await delay(2000);
  } finally {
    console.log("done");
  }
}
console.log("start")
demo(new CancelToken(cancel => {
  delay(1000).then(() => {
    console.log("cancelling");
    cancel();
  });
}))

Will "done" be logged after one or two seconds? Clearly there is code running concurrently to the async function and the two-second delay. I am assuming on purpose here that cancelToken is not passed to the delay(2000) call and that promise doesn't get cancelled.

domenic commented 8 years ago

Two seconds. Because here is what the VM does:

NOW execution of the function is suspended for 2 seconds, and we can execute other code (such as console.log("cancelling"); cancel();. We cannot do so before this point, so it's pointless to insert an implicit cancelToken.cancelIfRequested() before the await delay(2000);.

And yes, cancel() will be executed at the one second mark. But the async function is suspended for 2 seconds, so there's no way for it to know about that. If you want to cancel during the suspension, you have to turn over the cancel token to the operation that causes the suspension, by doing await delay(2000, cancelToken).

(This is why it is so hard to find a good use case for cancelIfRequested() or await.cancelToken, by the way: it is almost always better to just pass the cancel token along to the child operation.)

bergus commented 8 years ago

But the async function is suspended for 2 seconds, so there's no way for it to know about that.

Sure there is - that's what we registered the cancelToken for through await.cancelToken. We could desugar await x to

cancelToken.cancelIfRequested()
await Promise.race(x, cancelToken.promise.then(c => { cancel throw c; }));

which would cause "done" to be logged after only 1s in the above example.

Isn't that what we want, to break and run our finally clauses as early as possible after the token is cancelled?

domenic commented 8 years ago

That's an interesting proposal as well, thank you. Although Promise.race only takes one argument, and ignores canceled promises anyway, I think I understand what you're getting at.

bergus commented 8 years ago

Ah, really should've studied https://domenic.github.io/cancelable-promise/#sec-promise.race better, I was under the mistaken assumption that cancellation worked like rejection there.

bergus commented 8 years ago

Just a quick update, I've changed await.cancelOn (or whatever we name it) to a nullary/unary operator in my draft. I believe that's more attractive and signifies the special behaviour (influencing awaits around the whole function) better than a "property" with an assignment-like behaviour. And it's probably also easier to specify.

The best we could get from a clarity perspective might be a scope-like behaviour:

await.cancellation from cancelToken in {
    await …;
}

(but we would need to consider what to do with nesting, maybe even allow token composition through that)

domenic commented 8 years ago

The reason it's going to be specified as await.cancelToken = is because it fits with the existing meta-property concept used elsewhere. There is no dotted unary operator concept so we won't be introducing one.

bergus commented 8 years ago

You sound like it's decided already? The only meta properties I know about are new.target and function.first (in generators, I might misremember the name), and those act purely as "getters" - or plain keywords/nullary operators, rather. It seems that assignment to them introduces a new concept anyway, doesn't it?

domenic commented 8 years ago

Yeah, it was decided at the last TC39 meeting.

bergus commented 8 years ago

Ah, I had interpreted the meeting notes as only pursuing this path further, not to settle on the exact syntax. Anyway, I should be glad my idea was adopted :-)

bergus commented 8 years ago

Wow, thanks! I guess I should open seperate issues for any problems I see with that draft or whishes I have to extend it?

domenic commented 8 years ago

Yeah, that would be best.