Is there a way to short circuit async/await flow?

前端 未结 7 2046
名媛妹妹
名媛妹妹 2020-12-02 11:19
async function update() {
   var urls = await getCdnUrls();
   var metadata = await fetchMetaData(urls);
   var content = await fetchContent(metadata);
   await rend         


        
相关标签:
7条回答
  • 2020-12-02 11:34

    Example written in Node with Typescript of a call which can be aborted from outside:

    function cancelable(asyncFunc: Promise<void>): [Promise<void>, () => boolean] {
      class CancelEmitter extends EventEmitter { }
    
      const cancelEmitter = new CancelEmitter();
      const promise = new Promise<void>(async (resolve, reject) => {
    
        cancelEmitter.on('cancel', () => {
          resolve();
        });
    
        try {
          await asyncFunc;
          resolve();
        } catch (err) {
          reject(err);
        }
    
      });
    
      return [promise, () => cancelEmitter.emit('cancel')];
    }
    

    Usage:

    const asyncFunction = async () => {
      // doSomething
    }
    
    const [promise, cancel] = cancelable(asyncFunction());
    
    setTimeout(() => {
      cancel();
    }, 2000);
    
    (async () => await promise)();
    
    0 讨论(0)
  • 2020-12-02 11:42

    I just gave a talk about this - this is a lovely topic but sadly you're not really going to like the solutions I'm going to propose as they're gateway-solutions.

    What the spec does for you

    Getting cancellation "just right" is actually very hard. People have been working on just that for a while and it was decided not to block async functions on it.

    There are two proposals attempting to solve this in ECMAScript core:

    • Cancellation tokens - which adds cancellation tokens that aim to solve this issue.
    • Cancelable promise - which adds catch cancel (e) { syntax and throw.cancel syntax which aims to address this issue.

    Both proposals changed substantially over the last week so I wouldn't count on either to arrive in the next year or so. The proposals are somewhat complimentary and are not at odds.

    What you can do to solve this from your side

    Cancellation tokens are easy to implement. Sadly the sort of cancellation you'd really want (aka "third state cancellation where cancellation is not an exception) is impossible with async functions at the moment since you don't control how they're run. You can do two things:

    • Use coroutines instead - bluebird ships with sound cancellation using generators and promises which you can use.
    • Implement tokens with abortive semantics - this is actually pretty easy so let's do it here

    CancellationTokens

    Well, a token signals cancellation:

    class Token {
       constructor(fn) {
          this.isCancellationRequested = false; 
          this.onCancelled = []; // actions to execute when cancelled
          this.onCancelled.push(() => this.isCancellationRequested = true);
          // expose a promise to the outside
          this.promise = new Promise(resolve => this.onCancelled.push(resolve));
          // let the user add handlers
          fn(f => this.onCancelled.push(f));
       }
       cancel() { this.onCancelled.forEach(x => x); }
    }
    

    This would let you do something like:

    async function update(token) {
       if(token.isCancellationRequested) return;
       var urls = await getCdnUrls();
       if(token.isCancellationRequested) return;
       var metadata = await fetchMetaData(urls);
       if(token.isCancellationRequested) return;
       var content = await fetchContent(metadata);
       if(token.isCancellationRequested) return;
       await render(content);
       return;
    }
    
    var token = new Token(); // don't ned any special handling here
    update(token);
    // ...
    if(updateNotNeeded) token.cancel(); // will abort asynchronous actions
    

    Which is a really ugly way that would work, optimally you'd want async functions to be aware of this but they're not (yet).

    Optimally, all your interim functions would be aware and would throw on cancellation (again, only because we can't have third-state) which would look like:

    async function update(token) {
       var urls = await getCdnUrls(token);
       var metadata = await fetchMetaData(urls, token);
       var content = await fetchContent(metadata, token);
       await render(content, token);
       return;
    }
    

    Since each of our functions are cancellation aware, they can perform actual logical cancellation - getCdnUrls can abort the request and throw, fetchMetaData can abort the underlying request and throw and so on.

    Here is how one might write getCdnUrl (note the singular) using the XMLHttpRequest API in browsers:

    function getCdnUrl(url, token) {
        var xhr = new XMLHttpRequest();
        xhr.open("GET", url);
        var p = new Promise((resolve, reject) => {
          xhr.onload = () => resolve(xhr);
          xhr.onerror = e => reject(new Error(e));
          token.promise.then(x => { 
            try { xhr.abort(); } catch(e) {}; // ignore abort errors
            reject(new Error("cancelled"));
          });
       });
       xhr.send();
       return p;
    }
    

    This is as close as we can get with async functions without coroutines. It's not very pretty but it's certainly usable.

    Note that you'd want to avoid cancellations being treated as exceptions. This means that if your functions throw on cancellation you need to filter those errors on the global error handlers process.on("unhandledRejection", e => ... and such.

    0 讨论(0)
  • 2020-12-02 11:44

    This answer I posted may help you to rewrite your function as:

    async function update() {
       var get_urls = comPromise.race([getCdnUrls()]);
       var get_metadata = get_urls.then(urls=>fetchMetaData(urls));
       var get_content = get_metadata.then(metadata=>fetchContent(metadata);
       var render = get_content.then(content=>render(content));
       await render;
       return;
    }
    
    // this is the cancel command so that later steps will never proceed:
    get_urls.abort();
    

    But I am yet to implement the "class-preserving" then function so currently you have to wrap every part you want to be able to cancel with comPromise.race.

    0 讨论(0)
  • 2020-12-02 11:49

    Unfortunately, no, you can't control execution flow of default async/await behaviour – it does not mean that the problem itself is impossible, it means that you need to do change your approach a bit.

    First of all, your proposal about wrapping every async line in a check is a working solution, and if you have just couple places with such functionality, there is nothing wrong with it.

    If you want to use this pattern pretty often, the best solution, probably, is to switch to generators: while not so widespread, they allow you to define each step's behaviour, and adding cancel is the easiest. Generators are pretty powerful, but, as I've mentioned, they require a runner function and not so straightforward as async/await.

    Another approach is to create cancellable tokens pattern – you create an object, which will be filled a function which wants to implement this functionality:

    async function updateUser(token) {
      let cancelled = false;
    
      // we don't reject, since we don't have access to
      // the returned promise
      // so we just don't call other functions, and reject
      // in the end
      token.cancel = () => {
        cancelled = true;
      };
    
      const data = await wrapWithCancel(fetchData)();
      const userData = await wrapWithCancel(updateUserData)(data);
      const userAddress = await wrapWithCancel(updateUserAddress)(userData);
      const marketingData = await wrapWithCancel(updateMarketingData)(userAddress);
    
      // because we've wrapped all functions, in case of cancellations
      // we'll just fall through to this point, without calling any of
      // actual functions. We also can't reject by ourselves, since
      // we don't have control over returned promise
      if (cancelled) {
        throw { reason: 'cancelled' };
      }
    
      return marketingData;
    
      function wrapWithCancel(fn) {
        return data => {
          if (!cancelled) {
            return fn(data);
          }
        }
      }
    }
    
    const token = {};
    const promise = updateUser(token);
    // wait some time...
    token.cancel(); // user will be updated any way
    

    I've written articles, both on cancellation and generators:

    • promise cancellation
    • generators usage

    To summarize – you have to do some additional work in order to support canncellation, and if you want to have it as a first class citizen in your application, you have to use generators.

    0 讨论(0)
  • 2020-12-02 11:51

    Just like in regular code you should throw an exception from the first function (or each of the next functions) and have a try block around the whole set of calls. No need to have extra if-elses. That's one of the nice bits about async/await, that you get to keep error handling the way we're used to from regular code.

    Wrt cancelling the other operations there is no need to. They will actually not start until their expressions are encountered by the interpreter. So the second async call will only start after the first one finishes, without errors. Other tasks might get the chance to execute in the meantime, but for all intents and purposes, this section of code is serial and will execute in the desired order.

    0 讨论(0)
  • 2020-12-02 11:53

    Here is a simple exemple with a promise:

    let resp = await new Promise(function(resolve, reject) {
        // simulating time consuming process
        setTimeout(() => resolve('Promise RESOLVED !'), 3000);
        // hit a button to cancel the promise
        $('#btn').click(() => resolve('Promise CANCELED !'));
    });
    

    Please see this codepen for a demo

    0 讨论(0)
提交回复
热议问题