Beautiful way to resolve an object with nested promises? - javascript

While building custom endpoints I often need to resolve a complex object containing promises.
For illustration, take this example:
Given known user's id, employeeId and memberGroupsIds (an array):
var loginResponse = {
userprofile : getProfile(id)
companyInfo : {
company : getCompany(employeeId)
companyRelations : getPriviligedInfo(employeeId)
}
groups : getGroups(memberGroupsIds)
}
This logic works for synchronous functions that just return their values. But with functions that return promises I have to manually push all of them into an array to ensure they are resolved before using the final object.
I find the above code very easy to understand, and I'm looking for a signature that gives some of that, while still ensuring that the promises are resolved before sending a final object to the client.
The problem is not making it work, but making it beautiful and easy to read.
The best answer would ensure that the values are returned to the expected keys in the object and that all the promises are resolved in parallel, while maintaining a structure that is somewhat compatible with that of synchronous functions.
Or, if I'm missing the point and looking at this all wrong, how should I be looking at it?

You could use the helper function below. It takes an object and returns a promise that resolves when all nested promises have been resolved. The returned promise will provide as value the same object, which will have mutated with all its embedded promises replaced by their corresponding values.
function promiseRecursive(obj) {
const getPromises = obj =>
Object.keys(obj).reduce( (acc, key) =>
Object(obj[key]) !== obj[key]
? acc
: acc.concat(
typeof obj[key].then === "function"
? [[obj, key]]
: getPromises(obj[key])
)
, []);
const all = getPromises(obj);
return Promise.all(all.map(([obj, key]) => obj[key])).then( responses =>
(all.forEach( ([obj, key], i) => obj[key] = responses[i]), obj)
);
}
You would call it like this:
var loginResponsePromise = promiseRecursive({
userprofile : getProfile(10),
companyInfo : {
company : getCompany(101),
companyRelations : getPriviligedInfo(101)
},
groups : getGroups([5])
});
function promiseRecursive(obj) {
const getPromises = obj =>
Object.keys(obj).reduce( (acc, key) =>
Object(obj[key]) !== obj[key] ? acc
: acc.concat(typeof obj[key].then === "function" ? [[obj, key]]
: getPromises(obj[key]))
, []);
const all = getPromises(obj);
return Promise.all(all.map(([obj, key]) => obj[key])).then( responses =>
(all.forEach( ([obj, key], i) => obj[key] = responses[i]), obj)
);
}
// Example promise-returning functions
const wait = ms => new Promise( resolve => setTimeout(resolve, ms) ),
getProfile = id => wait(100).then(_ => ({userName: 'user' + id,id})),
getCompany = employeeId => wait(200).then(_ => ({employeeName: 'employee' + employeeId, employeeId})),
getPriviligedInfo = employeeId => wait(500).then(_ => ({privs: 'privInfo' + employeeId, employeeId})),
getGroups = memberGroupsIds => wait(400).then(_ => ({groups: ['group' + memberGroupsIds[0]],memberGroupsIds}));
// Sample input passed to `promiseRecursive` function
const loginResponsePromise = promiseRecursive({
userprofile : getProfile(10),
companyInfo : {
company : getCompany(101),
companyRelations : getPriviligedInfo(101)
},
groups : getGroups([5])
});
// Display the resolved object
loginResponsePromise.then( o => console.log(o) );
.as-console-wrapper { max-height: 100% !important; top: 0; }

I usually solve this kind of scenarios with Bluebird's join http://bluebirdjs.com/docs/api/promise.join.html :
const Promise = require('bluebird');
return Promise.join(
getProfile(id),
getCompany(employeeId),
getPrivilegedInfo(employeeId),
getGroups(memberGroupsIds),
(userProfile, company, companyRelations, groups) => {
return {
userProfile: userProfile,
companyInfo: {
company: company,
companyRelations: companyRelations
},
groups: groups
};
}
);

Using new ES6 features I would write something like this:
Promise.all([
getProfile(id),
getCompany(employeeId),
getPriviligedInfo(employeeId),
getGroups(memberGroupsIds)
])
.then(response => {
const [ userprofile, company, companyRelations, groups ] = response
const loginResponse = {
userprofile,
companyInfo : {
company,
companyRelations
},
groups
}
})
.catch(err => console.error(err))
Maybe the interesting part is that Promise.all() keep the input arguments order not depending on which resolves first. So in next step, using Destructuring Array assignment, the code looks like synchronous.

Related

How to pass variable from each promise to a Promise.allSettled?

In my project (VUE + Vuex) I need to make some API requests simultaneously, according to some contents and then process the results.
The getters.api_props(key) function will return the method ('post', 'patch', 'delete') or false if there is no need for a request. It will also return the url and the object that is needed for the request.
The api method returns the request as a Promise using axios.
Here is my code so far:
var contents = {person: {...}, info: {...}}
var promiseArray = [];
for (var key in contents) {
let [method, url, hash] = getters.api_props(key);
if (method) { promiseArray.push(api[method](url, hash)) }
}
await Promise.allSettled(promiseArray).then((results) => {
results.map(r => {
// THE RESULTS WILL BE PROCESSED HERE like:
// commit("save", [key, r])
console.info(r)
})
}).catch(e => console.log('ERROR:::',e)).finally(commit("backup"))
The problem is that the results does not include the 'key' so the save method that is called cannot know where to save the results.
Can you propose a fix or a better solution?
I would recommend to write
const contents = {person: {...}, info: {...}}
cosnt promiseArray = [];
for (const key in contents) {
let [method, url, hash] = getters.api_props(key);
if (method) {
promiseArray.push(api[method](url, hash)).then(value => ({
key,
status: 'fulfilled',
value
}), reason => ({
key,
status: 'rejected',
reason
})))
}
}
const results = await Promise.all(promiseArray);
for (const r of results) {
if (r.status=='fulfilled') {
console.info(r.key, ':', r.value.data)
commit("save", [r.key, r.value]);
} else if (r.status=='rejected') {
console.warn(r.key, ':', r.reason)
}
})
commit("backup");
So, to answer my own question, after Bergi's comments I filled promiseArray with
api[method](url, hash).then((r) => [key, r]).catch((e) => {throw [key, e.response]})
and then found the key that I needed:
await Promise.allSettled(promiseArray).then((results) => {
results.map((r) => {
if (r.status=='fulfilled') {
console.info(r.value[0],':',r.value[1].data)
}
if (r.status=='rejected') {
console.warn(r.reason[0],':',r.reason[1])
}
})
})
You obviously don't need to take this, but I fiddled with it for a while and this is what I liked best:
import forEach from 'lodash/forEach'
import mapValues from 'lodash/mapValues'
import { api, getters } from 'somewhere'
var contents = {person: {...}, info: {...}}
const promiseContents = mapValues(contents, (value, key) => {
let [method, url, hash] = getters.api_props(key);
if (!method) { return }
return api[method](url, hash)
})
await Promise.allSettled(Object.values(promiseContents))
forEach(promiseContents, (promise, key) => {
promise.then(response => {
if (promise.status === 'rejected') {
console.warn(key, ':', response)
}
console.info(key, ':', value.data)
})
})
The big requirement is that you include lodash in the project, but that is not an unusual ask in javascript projects.
mapValues allows you to keep the structure of your contents object while replacing the values with promises. I just use await on Promise.allSettled to tell the rest of the code when it can go. I just ignore the results.
Finally, using lodash's forEach I interpret the results. The advantage here is that every promise is run in a function alongside the key from your original contents object.
I like doing it this way because it doesn't require you to create a [key, result] array. That said, either way works fine.

Array.filter() with async arrow function

I am trying to filter my array using Array.filter() function, however I came across this issue.
I need to call some other function inside the filter function asynchronously. However the array is not changing its value based on the conditions that I define in the function.
const filterWithEmail = data.filter(async (doc) =>
{
const findUser = await UserService.findUser(doc.id).catch(err => {});
if (findUser)
{
const { email } = findUser;
return regexFilter ? regexFilter.test(email.normalize("NFKC")) : false;
}
});
This code doesn't affect the data array at all for some reason.
Any suggestions what I'm doing wrong?
Thank you in advance.
filter expects the return value of the callback to be a boolean but async functions always return a promise.
You don't know if you want to return true or false in time to tell filter which it is.
What you possibly want to do is:
map the data from data to { keep: true, original_data: data } (using an async callback)
Pass the resulting array of promises to Promise.all
await the return value of Promise.all
filter that array with: .filter(data => data.keep)
Get the original objects back with .map(data => data.original_data)
Something along these lines (untested):
const filterWithEmail = (
await Promise.all(
data.map(async (data) => {
const findUser = await UserService.findUser(doc.id).catch((err) => {});
let keep = false;
if (findUser && regexFilter)
keep = regexFilter.test(email.normalize("NFKC"));
return { data, keep };
})
)
)
.filter((data) => data.keep)
.map((data) => data.data);

Multiple queries end with unpredictably sorted result

There are few hundred documents in my database. Schema is very simple:
var firmsSchema = mongoose.Schema({
name: String,
sections: [String],
});
I want to query documents and iterate over:
{{#each sections}}
{{sectionName}}
{{#each firms}}
{{firmName}}
{{/each}}
{{/each}}
Simple:
const SECTIONS = ['name_one', 'name_two', 'name_three'];
const UNSORTED_SECTION_NAME = 'unsorted';
router.get('/', function(req, res, next) {
var showFirms = function showSection (i, acc) {
if (i < 0) return;
let query = SECTIONS[i] ? {sections: SECTIONS[i]} : {sections: {$nin: SECTIONS}};
let key = SECTIONS[i] || UNSORTED_SECTION_NAME;
Firms.find(query).
then((result) => {
acc.push({
section: key,
firms: result,
});
if (i === SECTIONS.length) {
acc = acc.sort((a, b) => (a.section > b.section));
res.render('template', {
sections: acc,
});
}
}).
then(showSection (i - 1, acc));
}
showFirms(SECTIONS.length, []);
};
Works fine. Except it returns acc randomly and unpredictably sorted. I mean 'name_two' section can follow 'name_one' or vise versa.
I thought .sort() at the end of promises chain would be a silver bullet here and solve all asynchronous problems, but it didn't.
Of course i can sort acc with handlebars helper after i pass it to my template, but it is so ridiculously strange i can't sort it right after all queries have been done in my showFirms function.
Can you give me some advise please?
Look at this remake of your code. Instead of getting the data one by one, we gotta get them on the same time (asynchronously) and then treat the return.
If you have any questions I am here, this code is untested so give me a feedback. This is an example of how you can change your code.
const showFirms = function showSection() {
return new Promise((resolve, reject) => {
// Get the keys for the queries
const keys = SECTIONS.map(x => x || UNSORTED_SECTION_NAME);
// For each sections we gonna call a find request
const promises = SECTIONS.map((x, xi) => {
const query = x ? {
sections: x,
} : {
sections: {
$nin: SECTIONS,
},
};
const key = keys[xi];
return Firms.find(query);
});
// Resolve all promises
Promise.all(promises)
.then((rets) => {
// Use the finds results to build an acc array
const accs = rets.map((x, xi) => ({
section: keys[xi],
firms: x,
}));
// Change the sort -> ;) #comments
const sortedAccs = accs.sort((a, b) => (a.section > b.section));
resolve(sortedAccs);
})
.catch(reject);
});
};
How to use it
showFirms()
.then(accs => res.render('template', {
sections: accs,
}))
.catch(err => console.log(`I have an error ${err.toString()}`));
Based on Grégory NEUTS excellent solution. I simplified some things and made the "other sections case" work. I even dropped out sort functionality. Eventual result returns in order of sections as they were declared in the initial SECTIONS array, so now i can just reorder it to control output.
const showFirms = function () {
return new Promise ((resolve, reject) => {
const extendedSections = SECTIONS.slice();
extendedSections.push(UNSORTED_SECTION_NAME);
const promises = extendedSections.map((section) => {
const unsortedCase = section === UNSORTED_SECTION_NAME;
const query = unsortedCase ? {sections: {$nin: SECTIONS}} : {sections: section};
return Firms.find(query);
})
Promise.all(promises)
.then((allResponces) => {
const sectionsData = allResponces.map((response, i) => ({
section: extendedSections[i],
firms: response,
}));
resolve(sectionsData);
})
.catch(reject);
});
};

Equivalent of BlueBird Promise.props for ES6 Promises?

I would like to wait for a map of word to Promise to finish. BlueBird has Promise.props which accomplishes this, but is there a clean way to do this in regular javascript? I think I can make a new object which houses both the word and the Promise, get an array of Promises of those objects, and then call Promise.all and put them in the map, but it seems like overkill.
An implementation of Bluebird.props that works on plain objects:
/**
* This function maps `{a: somePromise}` to a promise that
* resolves with `{a: resolvedValue}`.
* #param {object} obj
* #returns {Promise<object>}
*/
function makePromiseFromObject(obj) {
const keys = Object.keys(obj);
const values = Object.values(obj);
return Promise.all(values)
.then(resolved => {
const res = {};
for (let i = 0; i < keys.length; i += 1) {
res[keys[i]] = resolved[i];
}
return res;
})
}
If you are dealing with a Map with values that are promises (or a mix of promises and non-promises) - and you want the final resolved value to be a Map with all values resolved
const mapPromise = map =>
Promise.all(Array.from(map.entries()).map(([key, value]) => Promise.resolve(value).then(value => ({key, value}))))
.then(results => {
const ret = new Map();
results.forEach(({key, value}) => ret.set(key, value));
return ret;
});
Although, I bet someone has a slicker way to do this, some of the new ES2015+ stuff is still new to me :p
The venerable async.js library has a promisified counterpart: async-q
The promisified async-q library supports all the functions in the async library. Specifically async.parallel(). At first glance async.parallel() looks just like Promise.all() in accepting an array of functions (note one difference, an array of functions, not promises) and run them in parallel. What makes async.parallel special is that it also accepts an object:
const asyncq = require('async-q');
async function foo () {
const results = await asyncq.parallel({
something: asyncFunction,
somethingElse: () => anotherAsyncFunction('some argument')
});
console.log(results.something);
console.log(results.somethingElse);
}
Alternative implementation combining ES6+ Object.entries() and Object.fromEntries():
async function pprops(input) {
return Object.fromEntries(
await Promise.all(
Object.entries(input)
.map(
([k, p])=>p.then(v=>[k, v])
)
)
);
};
I have two different implementations using ES6 async functions:
async function PromiseAllProps(object) {
const values = await Promise.all(Object.values(object));
Object.keys(object).forEach((key, i) => object[key] = values[i]);
return object;
}
One line shorter, but less optimized:
async function PromiseAllProps(object) {
const values = await Promise.all(Object.values(object));
return Object.fromEntries(Object.keys(object).map((prop, i) => ([prop, values[i]])));
}
Example
const info = await PromiseAllProps({
name: fetch('/name'),
phone: fetch('/phone'),
text: fetch('/foo'),
});
console.log(info);
{
name: "John Appleseed",
phone: "5551234",
text: "Hello World"
}
It would be advisable to use a library like bluebird for this. If you really want to do this yourself, the main idea is to:
Resolve each of the map values and connect the promised value back with the corresponding key
Pass those promises to Promise.all
Convert the final promised array back to a Map
I would make use of the second argument of Array.from, and the fact that an array of key/value pairs can be passed to the Map constructor:
Promise.allMap = function(map) {
return Promise.all( Array.from(map,
([key, promise]) => Promise.resolve(promise).then(value => [key, value])
) ).then( results => new Map(results));
}
// Example data
const map = new Map([
["Planet", Promise.resolve("Earth")],
["Star", Promise.resolve("Sun")],
["Galaxy", Promise.resolve("Milky Way")],
["Galaxy Group", Promise.resolve("Local Group")]
]);
// Resolve map values
Promise.allMap(map).then( result => console.log([...result]) );
.as-console-wrapper { max-height: 100% !important; top: 0; }
You can simply write it using Promise.all + reduce
const promiseProps = (props) => Promise.all(Object.values(props)).then(
(values) => Object.keys(props).reduce((acc, prop, index) => {
acc[prop] = values[index];
return acc;
}, {})
);
And a nice lodash variant for sake of completeness for plain js objects
async function makePromiseFromObject(obj: {[key: string]: Promise<any>}) {
return _.zipObject(Object.keys(obj), await Promise.all(Object.values(obj)))
}

JavaScript - ES6 function does not work for some weird reason

I am setting the below function in order to retrieve it after.
However, for some reason, it does not work:
constructor(private nativeStorage: NativeStorage) {
// I am calling it this way:
this.userId = 123;
this.getItem("userId", this.userId);
// If I replace the shortcut by the below it works fine
/* this.nativeStorage.getItem("userId).then(
data => this.userId = data,
error => console.error(error)
); */
}
getItem(itemKey, itemValue) {
return this.nativeStorage.getItem(itemKey).then(
data => itemValue = data,
error => console.error(error)
);
}
I believe that I am missing something here, that's why it doesn't work
You're assigning data to itemValue which is just a copy of this.userId. JS doesn't support pass by reference that could make that possible. Instead you can just use itemKey to assign to the class instance directly like this:
getItem(itemKey) {
return this.nativeStorage.getItem(itemKey).then(
data => this[itemKey] = data, // assign data to this[itemKey] which will be this.userId (in the example above)
error => console.error(error)
);
}
JavaScript is not supporting call by reference. It's only by value arg passing. So values can be updated only by object references only.
In reference to the part/comments where you can't have an async constructor and therefore I extract all the async parts into a factory.
I have no experience yet with ionic, so you should take the following as pseudocode:
import { NativeStorage } from '#ionic-native/native-storage';
function constructor(public userId: String){ }
//and the factory function
//don't know how exactly how to export stuff in that framework
function createById(userId){
return NativeStorage.getItem('userId')
.then(
userId => new YourClass(userId)
error => console.error(error)
)
}
or in the case where you want to assign multiple properties:
//a utility to resolve with { key: Promise<value> } mappings + pretty much everything you throw at it.
//warning: doesn't resolve promises in nested structures (by design),
//stuff like {key: { foo: Promise<value> }}
//ideally you'd put that in a seperate module since it can be handy in many places.
function resolve(obj){
if(Array.isArray(obj)
return Promise.all(obj);
if(typeof obj === "function")
return Promise.resolve().then(obj);
if(!obj || typeof obj !== "object" || "then" in obj && typeof obj.then === "function")
return Promise.resolve(obj);
var keys = Object.keys(obj);
return Promise.all( keys.map(k => obj[k]) )
.then(values => combine(keys, values));
}
//takes two Arrays, a matching set of keys and values and combines that into an object.
function combine(keys, values){
return keys.reduce((acc, key, index) => {
acc[key] = values[index];
return acc;
}, {});
}
const assignTo = target => source => Object.assign(target, source);
//in this case we use assign all async values outside of the constructor
function constructor(){
this.userId = null;
this.userName = null;
}
function createById(userId){
return resolve({
userId: NativeStorage.getItem('userId'),
userName: NativeStorage.getItem('userName'),
//...
}).then(
assignTo( new YourClass() ),
error => console.error(error)
)
}
Or if this is still to much repetition for you:
//a utility to fetch multiple items at once
function getItems(...keys){
return Promise.all( keys.map( key => NativeStorage.getItem(key) ) )
.then(values => combine(keys, values));
}
//and a quick test
getItems("userId", "userName", ...).then(console.log);
//or in the context of the last snippet:
getItems("userId", "userName", ...).then(assignTo( new YourClass() ));
Hope this helps shows you a different approach to your problems.

Categories