getify / CAF

Cancelable Async Flows (CAF)
MIT License
1.34k stars 46 forks source link

explore: should CAF support "async generators"? #3

Closed getify closed 5 years ago

getify commented 6 years ago

For the new async function* async generator functions that just landed in ES2018... should CAF support wrapping them (for cancelation) the same way we wrap a regular function * generators?

The implementation would be fairly straightforward... when you call it.next(..) in _runner(..), you just have to test if that result is itself a promise, and if so, wait for the actual iterator result from that promise before handing off to processResult(..) for processing.

The problem is... just like with regular async functions, if an async function* is currently awaiting a promise, doing it.return(..) doesn't immediately abort. It schedules an abort of the function, but only takes effect after the current await on a promise finishes.

In other words, using CAF with an async function* will give the appearance of the ability to do cancelations, but it will perhaps be a surprising detail that they can't necessarily be immediately canceled the way function * generators can, depending on what the async function* is currently doing.

That kind of surprising inconsistency might be more harmful to CAF supporting these, and maybe that means CAF shouldn't handle them. OTOH, handling them for some notion of cancelation might be better than nothing.

Anyone have any thoughts?


Illustration code:

function delay(ms) { return new Promise(res => setTimeout(res,ms)); }

async function *foo() {
   try {
      await delay(2000);
      return 42;
   }
   finally {
      return 50;
   }
}

var it = foo();
var res = it.next();
res.then(console.log);

var other = it.return(10);   // runs now, but doesn't cancel immediately, only schedules it
other.then(console.log);

// 2 seconds go by
// {value: 50, done: true}
// {value: 10, done: true}
getify commented 6 years ago

More thoughts:

  1. If an async function* does a yield pr instead of await pr, then CAF can cancel immediately and just discard/ignore that yielded promise.

    Of course, await pr in your async function* is kinda the whole point of them... if you didn't want to use await pr, then you could/should just use normal function*. So suggesting this as a "work-around" would kinda just be suggesting an anti-pattern.

  2. You don't need to use async function* for emulating cancelable async... that's what function * does. There's no benefit to choosing to use async function* for this synchronous-async pattern. So maybe CAF just doesn't need to support them because it's sorta pointless/moot to support them.

getify commented 5 years ago

I have decided finally... no we won't support async generators. The tipping point is, yield x in an async generator implicitly awaits the value if it's a promise, so... IOW there's no way for CAF to truly stop/finalize an async generator instance with a Promise.race(..) over the yielded value, the way we can do with normal generators.