3

I have a couple of async functions that get and store a value if it has not already been provided.

if (!output.valueA) {
  output.valueA = await getValueA();
}
  
if (!output.valueB) {
  output.valueB = await getValueB();
}

I know I can use Promise.all() to wait for both of these calls asynchronously, but what would be the best way to do this while not knowing whether one or other, or both calls, are even required, and then storing the results in the correct variables?

DBPaul
  • 500
  • 5
  • 16

3 Answers3

3

You can create an array with all the promises, filter it by presence of a value, and then use Promise.all. You'll also need a 1-to-1 map of property keys to async functions that used to get value for that key:

const fns = {
  "valueA": getValueA,
  "valueB": getValueB,
};
const keys = Object.keys(fns);
// all keys

const missing = keys.filter((key) => !output[key]);
// keys with missing values

const promises = missing.map((key) => fns[key]());
// an array of promises

const results = await Promise.all(promises);
// promise results, obviously

for (const [ index, key ] of missing.entries()) { // using Array.prototype.entries
  output[key] = results[index];
}
Parzh from Ukraine
  • 7,999
  • 3
  • 34
  • 65
2

Without changing your example a lot you could use then to set a callback that sets the property when resolved. Store the resulting promise inside an array so you know when all promises are finished.

const promises = [];

if (!output.valueA) {
  promises.push(getValueA().then(value => output.valueA = value));
}
  
if (!output.valueB) {
  promises.push(getValueB().then(value => output.valueB = value));
}

await Promise.all(promises);

You can use a more dynamic aproach by defining the properties to check and their async getters up front. An example could be:

const asyncGetters = [["valueA", getValueA], ["valueB", getValueB]];

await Promise.all(
  asyncGetters
    .filter(([prop]) => !output[prop])
    .map(async ([prop, asyncGetter]) => output[prop] = await asyncGetter())
);

Here filter acts like your if statement, selecting only the elements that have a falsy value. map will make sure to collect the promises of the async map callbacks, so you can use await Promise.all() to wait for them all to finish.

3limin4t0r
  • 19,353
  • 2
  • 31
  • 52
  • You can also use `promises.push((async () => output.valueA = await getValueA())())` for the first code block, however I personally find an async IIFE a bit cryptic as function argument. – 3limin4t0r Sep 15 '20 at 16:45
1

const output = {
  valueA : null,
  valueB: null
};

async function getValueA() {
  await new Promise(r => setTimeout(r, 3000));
  output.valueA = 2;
  return output.valueA;
}

async function getValueB() {
  await new Promise(r => setTimeout(r, 3000));
  output.valueB = 5;
  return output.valueB;
}

async function getAllValues() {
  const promises = [output.valueA || getValueA(), output.valueB || getValueB()];
  await Promise.all(promises);
  console.log(output.valueA);
  console.log(output.valueB);
}

getAllValues();

You can pass Promise.all an array with actual values instead of promises, and it will just resolve immediately. No need to make sure that they're all promises. Try running my code above, but changing output.valueA and valueB to be a number to begin with -- it will resolve immediately.

[edit]

read OPs comment and did this for illustrative purposes:

const output = {
  valueA : null,
  valueB: null
};

async function getValueA() {
  await new Promise(r => setTimeout(r, 3000));
  return 2;
}

async function getValueB() {
  await new Promise(r => setTimeout(r, 3000));
  return 5;
}

async function getAllValues() {
  const promises = [output.valueA || getValueA(), output.valueB || getValueB()];
  const [a, b] = await Promise.all(promises);
  output.valueA = a;
  output.valueB = b;
  console.log(output.valueA);
  console.log(output.valueB);
}

getAllValues();

You can use array destructuring to get the results of a Promise.all

TKoL
  • 13,158
  • 3
  • 39
  • 73
  • This requires `getValueA` and `getValueB` (and all the subsequent similar getters) to be redefined for each new value of `output` – Parzh from Ukraine Sep 15 '20 at 14:45
  • This only requires that I assumed the functions exist that exist in OPs question. – TKoL Sep 15 '20 at 14:48
  • I like this answer, but the functions `getValueA` and `getValueB` exist in a separate module, so it would be preferable to set the output values based on the result of the functions, rather than inside the functions themselves. – DBPaul Sep 15 '20 at 14:56