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 userId
s 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 userId
s.
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);
}