Home > Enterprise >  JavaScript - Promise.allSettled Array.reduce()
JavaScript - Promise.allSettled Array.reduce()

Time:05-03

Introduction

Imagine this method for getting the language of a user:

const getUserLanguage = (userId) => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);


(async () => {
    try {
        const language = await getUserLanguage("Mike")
        console.log(`Language: ${language}`);
    } catch(err) {
        console.error(err);
    }
})();

Now, I am trying to group the language of multiple users, performing a parallel request:

const getUserLanguage = () => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);

const groupUsersByLanguage = async (userIds) => {
    const promiseResults = await Promise.allSettled(
        userIds.reduce(async (acc, userId) => {
            const language = await getUserLanguage(userId);

            (acc[language] = acc[language] ?? []).push(userId);

            return acc;
        }, {})
    );
  
    console.log({ promiseResults });
  
    // Filter fulfilled promises
    const result = promiseResults
        .filter(({ status }) => status === "fulfilled")
        .map(({ value }) => value);
  
    return result;
}

(async () => {
    const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
    const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
    console.log(usersGroupedByLanguage);
})();

Problem

But my implementation is not working:

const promiseResults = await Promise.allSettled(
    userIds.reduce(async (acc, userId) => {
        const language = await getUserLanguage(userId);

        (acc[language] = acc[language] ?? []).push(userId);

        return acc;
    }, {})
);

How can I do for getting an output like

{
    "es": ["Mike", "Saul"],
    "en": ["Walter"],
}

using the Promise.allSettled combined with .reduce?

CodePudding user response:

Your .reduce is constructing an object where each value is a Promise. Such an object is not something that .allSettled can understand - you must pass it an array.

I'd create an object outside, which gets mutated inside a .map callback. This way, you'll have an array of Promises that .allSettled can work with, and also have the object in the desired shape.

const getLanguage = () => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);

const groupUsersByLanguage = async (userIds) => {
    const grouped = {};
    await Promise.allSettled(
        userIds.map(async (userId) => {
            const language = await getLanguage(userId);
            (grouped[language] = grouped[language] ?? []).push(userId);
        })
    );
    return grouped;
}

(async () => {
    const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
    const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
    console.log(usersGroupedByLanguage);
})();

An option that doesn't rely on side-effects inside a .map would be to instead return both the userId and the language inside the map callback, then filter the allSettled results to include only the good ones, then turn it into an object.

const getLanguage = () => new Promise(
    (resolve, reject) => {
        if (Math.random() < 0.3) resolve("en");
        if (Math.random() < 0.6) resolve("es");
        reject("Unexpected error.");
    }
);

const groupUsersByLanguage = async (userIds) => {
    const settledResults = await Promise.allSettled(
        userIds.map(async (userId) => {
            const language = await getLanguage(userId);
            return [userId, language];
        })
    );
    const grouped = {};
    settledResults
        .filter(result => result.status === 'fulfilled')
        .map(result => result.value)
        .forEach(([userId, language]) => {
            (grouped[language] = grouped[language] ?? []).push(userId);
        });
    return grouped;
}

(async () => {
    const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
    const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
    console.log(usersGroupedByLanguage);
})();

CodePudding user response:

I would write a main function using two utility functions for this: one that groups a set of elements according to the result of a function, and one that takes a predicate function and partitions an array into those ones for which it returns true and those ones for which it returns false. These two in turn use a push utility function which simply reifies Array.prototype.push into a plain function.

The main function maps the getUserLanguage function over the users, calls Promise.allSettled on the results, then we map over the resulting promises, to connect the original userId back with the promise results. (If the fake getUserLanguage returned an object with properties for both the userId and language, this step would be unnecessary.) Then we partition the resulting promises to separate out the fulfilled from the rejected ones. I do this because your question doesn't say what to do with the rejected language lookups. I choose to add one more entry to the output. Here as well as es and en, we also get a list of userIds under _errors. If we wanted to ignore these, then we could replace the partition with a filter and simplify the last step. That last step takes successful results and the failures, combining the successful ones into an object with our group helper, and appending the _errors, by mapping the failures to their userIds.

It might look like this:

// dummy implementation, resolving to random language, or rejecting with error
const getUserLanguage = (userId) => new Promise ((resolve, reject) => {if (Math.random() < 0.3) resolve("en"); if (Math.random() < 0.6) resolve("es"); reject("Unexpected error.");});
 

// utility functions
const push = (x) => (xs) => 
  (xs .push (x), xs)
const partition = (fn) => (xs) =>
  xs .reduce (([y, n], x) => fn (x) ? [push (x) (y), n] : [y, push (x) (n)], [[], []])
const group = (getKey, getValue) => (xs) => 
  xs .reduce ((a, x, _, __, key = getKey (x)) => ((a [key] = push (getValue (x)) (a[key] ?? [])), a), {})


// main function
const groupUsersByLanguage = (users) => Promise .allSettled (users .map (getUserLanguage))
  .then (ps => ps .map ((p, i) => ({...p, user: users [i]})))
  .then (partition (p => p .status == 'fulfilled'))
  .then (([fulfilled, rejected]) => ({
    ...group (x => x .value, x => x.user) (fulfilled),
    _errors: rejected .map (r => r .user)
  }))


// sample data
const users = ['fred', 'wilma', 'betty', 'barney', 'pebbles', 'bambam', 'yogi', 'booboo']


// demo
groupUsersByLanguage (users)
  .then (console .log)
.as-console-wrapper {max-height: 100% !important; top: 0}

This yields output like this (YMMV because of the random calls):

{
  en: [
    "fred",
    "wilma",
    "barney"
  ],
  es: [
    "bambam",
    "yogi",
    "booboo"
  ],
  _errors: [
    "betty",
    "pebbles"
  ]
}

Note that those utility functions are general-purpose. If we keep our own libraries of such tools handy, we can write functions like this without great effort.

CodePudding user response:

Another option of doing this would be to first fetch all languages using:

const languages = await Promise.allSettled(userIds.map(getLanguage));

Then zip then together with userIds and process them further.

async function getLanguage() {
  if (Math.random() < 0.3) return "en";
  if (Math.random() < 0.6) return "es";
  throw "Unexpected error.";
}

function zip(...arrays) {
  if (!arrays[0]) return;
  return arrays[0].map((_, i) => arrays.map(array => array[i]));
}

async function groupUsersByLanguage(userIds) {
  const languages = await Promise.allSettled(userIds.map(getLanguage));
    
  const groups = {};
  for (const [userId, language] of zip(userIds, languages)) {
    if (language.status != "fulfilled") continue;
    
    groups[language.value] ||= [];
    groups[language.value].push(userId);
  }
  
  return groups;
}

(async () => {
  const userIds = ["Mike", "Walter", "Saul", "Pinkman"];
  const usersGroupedByLanguage = await groupUsersByLanguage(userIds);
  console.log(usersGroupedByLanguage);
})();

If you are not interested in creating a zip() helper you can use a "normal" for-loop:

const groups = {};
for (let i = 0; i < userIds.length; i  = 1) {
  if (languages[i].status != "fulfilled") continue;
  
  groups[languages[i].value] ||= [];
  groups[languages[i].value].push(userId);
}
  • Related