Search code examples
javascriptdesign-patternsasync-awaitpromisedry

JS: Combining multiple independent promises into a single function


I have the following code appearing in many of my JavaScript files:

import { doA, doB, doC } from 'asyncDoer'

// ... other stuff

let isDoneA = false
let isDoneB = false
let isDoneC = false
doA.then(_ => isDoneA = true)
doB.then(_ => isDoneB = true)
doC.then(_ => isDoneB = true)

// ... other stuff

I want to implement Don't Repeat Yourself and put that block into a single exported function in asyncDoer. Something like doAll().

But no matter how much I think about it, I can't figure it out. Mainly because they are 2+ unrelated promises that resolve independently. One idea I had was to somehow pass isDoneA,B,C as references so the function modifies them on promise resolution. But I heard that modifing the source parameters is a bad idea that makes the code harder to read and mantain.

Can anyone help me in how you would make that block into a more concise, repeatable unit?


Solution

  • Seems you need Promise.all():

    export const isDone = {A: false, B: false, C:false, all: false};
    export function doAll(){
      return Promise.all([
        doA.then(_ => isDone.A = true),
        doB.then(_ => isDone.B = true),
        doC.then(_ => isDone.C = true)
        ]).then(_ => isDone.all = true);
    }
    

    Another variant:

    export function doAll(){
      const isDone = {A: false, B: false, C:false, all: false};
      
      return [isDone, Promise.all([
        doA.then(_ => isDone.A = true),
        doB.then(_ => isDone.B = true),
        doC.then(_ => isDone.C = true)
        ]).then(_ => isDone.all = true);
      ];
    }
    

    Usage:

    // you can still get the result, if you don't need, just leave isDone only
    const [isDone, promise] = doAll();
    promise.then(_ => /* do something after all the tasks are complete */ );
    
    

    It's not clear though how do you use isDone since there's no reactivity/callbacks. A callback version would like this:

    export function doAll(cb){
      
      return Promise.all([
        doA.then(_ => cb('A', _)),
        doB.then(_ => cb('B', _)),
        doC.then(_ => cb('B', _))
        ]).then(_ => cb('all'));
    }
    

    Usage:

    doAll((task, result) => {
      // do something based on the task completed
    });
    

    Regarding myself I often use async generators for stuff like this. For a generator you need rather Promise.race():

    const delay = result => new Promise(r => setTimeout(() => r(result), Math.random()*1000));
    
    async function* asyncAll(promises){
      promises = Object.entries(promises).map(([key, promise]) => {
        const task = promise.then(result => {
          promises.splice(promises.indexOf(task), 1);
          return [key, result];
        });
        return task;
      })
      while (promises.length) {
          yield Promise.race(promises);
      }
    }
    
    function doAll(){
      return asyncAll({
        A: delay('A result'),
        B: delay('B result'),
        C: delay('C result')
      });
    }
    
    (async () => {
      
      for await(const [step, result] of doAll()){
        console.log(step, result);
      }
      
    })();