Synchronously using forEach with $q - javascript

So I have a situation where I need to build an Object, then inject some items into that Object, and then make an API call and also inject the API's response into that Object. It's a complex Object so we'll just assume that's alright, but anyway, I think that I need to do these 3 things synchronously and using array.forEach means they run asynchronously.
I hope my example is simple enough to understand, but basically I do 3 things:
Create an empty Array of Classes/Classrooms
Loop through an Array of Class IDs and create an Object for each Class
Loop through an Array of Students and push them into a students Array inside the Class Object
Loop through an Array of Options and push them into an options array inside the Class Object
Finally, I have something that could look like this for each Class:
{
class_id: "abc123",
students: [{}, {}, {}],
options: [{}, {}, {}]
}
And finally, here is my code:
// Create Array of Class Objects
var classes = [];
function processArray(array, func) {
return $q(function(resolve, reject) {
array.forEach(function(item, index) {
func(item);
if (index === (array.length - 1)) resolve();
})
})
}
// Create Courier Objects
processArray(classIds, function(id) {
classes.push({class_id: id, students: [], options: []});
}).then(function(response) {
// Inject Students into each Class
processArray(students, function(students) {
_.find(classes, {'class_id': student.class_id}).students.push(student);
}).then(function(response) {
// Inject classOptions into each Class
processArray(classOptions, function(classOption) {
_.find(classes, {'class_id': classOption.class_id}).classOptions.push(classOption);
}).then(function(response) {
// Print the classes
console.log(classes);
})
})
});
I've create a function which does it synchronously but I'd like to know if anyone can think of a much, much cleaner and more efficient way of doing the above. It seems extremely hacky, and maybe I don't even need to do it synchronously if I arranged my functions correctly.

Working with Promise Based APIs that Return Arrays
The processArray function returns a promise that resolves to null.
//WRONG
//Returns a promise that resolves to null
//
function processArray(array, func) {
return $q(function(resolve, reject) {
array.forEach(function(item, index) {
func(item);
if (index === (array.length - 1)) resolve();
})
})
}
To return a promise that resolves to an array, use $q.all.
//RIGHT
//Returns a promise that resolves to an array
//
function processArray(array, func) {
var promiseArray = [];
array.forEach(function(item, index) {
promiseArray.push($q.when(func(item));
});
return $q.all(promiseArray);
}
In either case whether func(item) returns either a value or a promise, $q.when will return a promise.
Be aware that $q.all is not resilient. It will resolve fulfilled with an array of values or it will resolve rejected with the first error.
processArray(array, func)
.then( function onFulfilled(dataList) {
//resolves with an array
$scope.dataList = dataList;
}).catch( function onRejected(firstError) {
console.log(firstError);
});
For more information, see AngularJS $q Service API Reference.

you are using nested promises. These suck. One major advantage of promises is to avoid the 'callback nightmare' of deep nesting.
Use this syntax ...
promise1.then(function(response1) {
// do something with response 1 (and possibly create promise2 here based on response1 if required)
return promise2
}).then(function(response2) {
// do something with response 2
return promise3
}).then(function(response3) {
// do something with response 3
// do something with promise 3???
}).catch(function(errorResponse) {
// will trigger this on a failure in any of the above blocks
// do something with error Response
});

Related

Need help understanding the scope of this javascript variable

In javascript, within a VueJS SPA, I'm trying to create a method that will allow me to reduce redundant code by passing the Google Maps Places Service only a place_id and the fields I would like returned.
getPlaceDetails (place, fields) {
this.$refs.mapRef.$mapPromise.then((map) => {
var placesServices = new window.google.maps.places.PlacesService(map)
placesServices.getDetails({ placeId: String(place.place_id), fields: fields }, (result, status) => {
if (status === window.google.maps.places.PlacesServiceStatus.OK) {
alert(JSON.stringify(result))
return result
}
})
})
}
I'm calling the above method from within another method:
var place = this.getPlaceDetails(place, ['name', 'geometry', 'place_id'])
It is invoked successfully... and the alert shows the desired JSON.. but place is null. I've tried using
var vm = this
above
var placesServices
and assigning the result to an app level variable... even inside of a .then after the first promise... like so:
getPlaceDetails (place, fields) {
this.$refs.mapRef.$mapPromise.then((map) => {
var vm = this
var placesServices = new window.google.maps.places.PlacesService(map)
placesServices.getDetails({ placeId: String(place.place_id), fields: fields }, (result, status) => {
if (status === window.google.maps.places.PlacesServiceStatus.OK) {
alert(JSON.stringify(result))
vm.tempPlace = result
}
})
}).then(function () {
return this.tempPlace
})
}
How can I get the method to return the result object??
Promises
A promise is an object that will resolve (or reject) in some point in the future. This is necessary to execute asynchronous tasks (e.g. http-calls) that take an undefined amount of time to finish.
Promises can be chained i.e. get executed one after another. This is what the .then method does. With .then you pass a function that will be executed as soon as the promise is finished. This function will receive the object that was returned by the previous promise.
Your method
getPlaceDetails (place, fields) {
return this.$refs.mapRef.$mapPromise.then((map) => {
var vm = this;
var placesServices = new window.google.maps.places.PlacesService(map);
placesServices.getDetails({ placeId: String(place.place_id), fields: fields }, (result, status) => {
if (status === window.google.maps.places.PlacesServiceStatus.OK) {
alert(JSON.stringify(result));
return result;
}
});
});
}
This Method will return a promise that - at some point in the future - will yield the desired result.
When you want to call the method you get that promise and have to handle it, again by passing a function (using .then) that will be executed once the result is ready.
this.getPlaceDetails(...).then((result) => {
// handle your result
}}
Alternatively you could use the await operator to wait until the promise is finished:
var place = await this.getPlaceDetails(...);
Instead of returning the data, you may consider assigning the JSON to a Vue watched data variable, like so:
var somePlace = new Vue({
el: '#someEl',
data: {
message: 'Hello robwolf.io',
tempPlace: {} // <- variable waiting for your asynchronous data if it comes thru
},
methods: {
getPlaceDetails (place, fields) {
// [...your promise code here...]
}).then((result) => {
this.tempPlace = JSON.stringify(result)
// return this.tempPlace
})
// [...the .then function must also be an arrow function to have scope access to tempPlace...]

mapping a Promise function to an array does not persist results

I have an object with two arrays as properties:
I want to populate the arrays by running promises in series.
I fetch the result of the promises, and map a function to decorate all the items in my arrays.
While one array get populated and persist, the other get populated only while in the map function, but at the end the array is returned still empty.
Can you help to understand why?
I check the Promise is actually returned, and indeed in one case it works, not in the other.
this is my pseudo-code:
function formatMyObject( arrayOfIds ) {
// initialize the objet
var myObj = {
decorators = [],
nodes = []
...
}
// I map the Promise reconciliate() and push the results in the array:
return reconciliateNode(arrayOfIds)
.then( data => {
data.map( node => {
// I fetch results, and myObj.nodes
myObj.nodes.push( { ... })
})
})
return myObj
})
.then( myObj => {
// myObj.nodes is now a NON empty array
// I want to the same with myObj.decorators:
var data = myObj.nodes
// I think I am doing just as above:
data.map( node =>
decorateNode(node.source)
.then( decoration => {
decoration = decoration[node.source]
myObj['decorators'].push( {
...
} )
// I check: the array is NOT empty and getting populated:
console.log('myObj.decorators', myObj)
debugger
})
)
// instead now, just after the map() function, myObj.decorators is EMPTY!
console.log('myObj.decorators', myObj);
debugger
return myObj
)
... // other stuff
}
As in the second case the map callback returns a promise, that case is quite different from the first case.
In the second case, you would need to await all those promises, for which you can use Promise.all.
The code for the second part could look like this:
.then( myObj => {
return Promise.all(myObj.nodes.map(node => decorateNode(node.source)));
}).then(decorations => {
myObj.decorators = decorations.map(decoration => {
decoration = decoration[node.source];
return ({
...
});
})
console.log('myObj.decorators', myObj);
return myObj;
})

Holding a future value of a promise in a variable

I have a database that's calling for a list of recent messages. Each message is an object and is stored as an Array of these message objects in chatListNew.
Each message object has a property "from", which is the ID of the user who posted it. What I want to do, is loop through this Array and append the actual profile information of the "From" user into the object itself. That way when the Frontend receives the information, it has access to one specific message's sender's profile in that respective message's fromProfile property.
I thought about looping through each one and doing a Promise.All for every one, however, that's hugely expensive if only a handful over users posted hundreds of messages. It would make more sense to only run the mongoose query once for each user. So I invented a caching system.
However, I'm confused as to how to store the promise of a future value inside of an array element. I thought setting the "fromProfile" to the previously called promise would magically hold this promise until the value was resolved. So I used Promise.all to make sure all the promises were completed and then returned by results, but the promises I had stored in the arrays were not the values I had hoped for.
Here is my code:
//chatListNew = an array of objects, each object is a message that has a "from" property indicating the person-who-sent-the-message's user ID
let cacheProfilesPromises = []; // this will my basic array of the promises called in the upcoming foreach loop, made for Promise.all
let cacheProfilesKey = {}; // this will be a Key => Value pair, where the key is the message's "From" Id, and the value is the promise retrieving that profile
let cacheProfileIDs = []; // this another Key => Value pair, which basically stores to see if a certain "From" Id has already been called, so that we can not call another expensive mongoose query
chatListNew.forEach((message, index) => {
if(!cacheProfileIDs[message.from]) { // test to see if this user has already been iterated, if not
let thisSearch = User.findOne({_id : message.from}).select('name nickname phone avatar').exec().then(results => {return results}).catch(err => { console.log(err); return '???' ; }); // Profile retrieving promise
cacheProfilesKey[message.from] = thisSearch;
cacheProfilesPromises.push(thisSearch); // creating the Array of promises
cacheProfileIDs[message.from] = true;
}
chatListNew[index]["fromProfile"] = cacheProfilesKey[message.from]; // Attaching this promise (hoping it will become a value once promise is resolved) to the new property "fromProfile"
});
Promise.all(cacheProfilesPromises).then(_=>{ // Are all promises done?
console.log('Chat List New: ', chatListNew);
res.send(chatListNew);
});
And this is my console output:
Chat List New: [ { _id: '5b76337ceccfa2bdb7ff35b5',
updatedAt: '2018-08-18T19:50:53.105Z',
createdAt: '2018-08-18T19:50:53.105Z',
from: '5b74c1691d21ce5d9a7ba755',
conversation: '5b761cf1eccfa2bdb7ff2b8a',
type: 'msg',
content: 'Hey everyone!',
fromProfile:
Promise { emitter: [EventEmitter], emitted: [Object], ended: true } },
{ _id: '5b78712deccfa2bdb7009d1d',
updatedAt: '2018-08-18T19:41:29.763Z',
createdAt: '2018-08-18T19:41:29.763Z',
from: '5b74c1691d21ce5d9a7ba755',
conversation: '5b761cf1eccfa2bdb7ff2b8a',
type: 'msg',
content: 'Yo!',
fromProfile:
Promise { emitter: [EventEmitter], emitted: [Object], ended: true } } ]
Whereas I was hoping for something like:
Chat List New: [ { _id: '5b76337ceccfa2bdb7ff35b5',
updatedAt: '2018-08-18T19:50:53.105Z',
createdAt: '2018-08-18T19:50:53.105Z',
from: '5b74c1691d21ce5d9a7ba755',
conversation: '5b761cf1eccfa2bdb7ff2b8a',
type: 'msg',
content: 'Hey everyone!',
fromProfile:
Promise {name: xxx, nickname: abc... etc} },
{ _id: '5b78712deccfa2bdb7009d1d',
updatedAt: '2018-08-18T19:41:29.763Z',
createdAt: '2018-08-18T19:41:29.763Z',
from: '5b74c1691d21ce5d9a7ba755',
conversation: '5b761cf1eccfa2bdb7ff2b8a',
type: 'msg',
content: 'Yo!',
fromProfile:
{name: xxx, nickname: abc... etc} } ]
Thank you guys! Open to other ways of accomplishing this :)
Pete
When a Promise is assigned to a variable, that variable will always be a Promise, unless the variable is reassigned. You need to get the results of your Promises from your Promise.all call.
There's also no point to a .then that simply returns its argument, as with your .then(results => {return results}) - you can leave that off entirely, it doesn't do anything.
Construct the array of Promises, and also construct an array of from properties, such that each Promise's from corresponds to the item in the other array at the same index. That way, once the Promise.all completes, you can transform the array of resolved values into an object indexed by from, after which you can iterate over the chatListNew and assign the resolved value to the fromProfile property of each message:
const cacheProfilesPromises = [];
const messagesFrom = [];
chatListNew.forEach((message, index) => {
const { from } = message;
if(messagesFrom.includes(from)) return;
messagesFrom.push(from);
const thisSearch = User.findOne({_id : from})
.select('name nickname phone avatar')
.exec()
.catch(err => { console.log(err); return '???' ; });
cacheProfilesPromises.push(thisSearch);
});
Promise.all(cacheProfilesPromises)
.then((newInfoArr) => {
// Transform the array of Promises into an object indexed by `from`:
const newInfoByFrom = newInfoArr.reduce((a, newInfo, i) => {
a[messagesFrom[i]] = newInfo;
return a;
}, {});
// Iterate over `chatListNew` and assign the *resolved* values:
chatListNew.forEach((message) => {
message.fromProfile = newInfoByFrom[message.from];
});
});
A Promise is an object container, like a Array. The difference being that a Promise holds a value that will sometimes be.
So, since you do not know when the value will be resolved in Promise jargon, generally you tell the promise what to do with the value, when it is resolved.
So for example,
function (id) {
const cache = {}
const promise = expensiveQuery(id)
// promise will always be a promise no matter what
promise.then(value => cache[id] = value)
// After the callback inside then is executed,
// cache has the value you are looking for,
// But the following line will not give you the value
return cache[params.id]
}
Now, what you might do to fix that code is, return the promise when the query is run for the first time, or return the cached value.
// I moved this out of the function scope to make it a closure
// so the cache is the same across function calls
const cache = {}
function (id) {
if(cache[id]) return cache[id]
const promise = expensiveQuery(id)
// promise will always be a promise no matter what
promise.then(value => cache[id] = value)
// now we just return the promise, because the query
// has already run
return promise
}
Now you'll have a value or a promise depending on whether the function has already been called once before for that id, and the previous call has been resolved.
But that's a problem, because you want to have a consistent API, so lets tweak it a little.
// I moved this out of the function scope to make it a closure
// so the cache is the same across function calls
const cache = {}
function cachingQuery (id) {
if(cache[id]) return cache[id]
const promise = expensiveQuery(id)
// Now cache will hold promises and guarantees that
// the expensive query is called once per id
cache[id] = promise
return promise
}
Ok, now you always have a promise, and you only call the query once. Remember that doing promise.then doesn't perform another query, it simply uses the last result.
And now that we have a caching query function, we can solve the other problem. That is adding the result to the message list.
And also, we dont' want to have a cache that survives for too long, so the cache can't be right on the top scope. Let's wrap all this inside a cacheMaker function, it will take an expensive operation to run, and it will return a function that will cache the results of that function, based on its only argument.
function makeCacher(query) {
const cache = {}
return function (id) {
if(cache[id]) return cache[id]
const promise = query(id)
cache[id] = promise
return promise
}
}
Now we can try to solve the other problem, which is, assign the user to each message.
const queryUser = makeCacher((id) => User.findOne({_id : id})
.select('name nickname phone avatar')
.exec())
const fromUsers = chatListNew.map((message) => queryUser(message.from))
Promise.all(fromUsers)
.then(users =>
chatListNew.map(message =>
Object.assign(
{},
message,
{ fromProfile: users.find(x => x._id === message.from)})))
.then(messagesWitUser => res.json(messagesWitUser) )
.catch(next) // send to error handler in express

How to wait the operations until finishing they work using RXJS?

I need a way to wait the RXJS finish his work.
This is my function:
getLastOrderBeta() {
return this.db.list(`Ring/${localStorage.getItem('localstorage')}`, {
query: {
equalTo: false,
orderByChild: 'status',
limitToLast: 2,
}
})
.map((orders: any) => {
return orders.map((order: any) => {
order.userInfo = this.getProfile(order.userid);
return order;
});
}).do((s:any)=>{
console.log(s);
console.log(here);
});
}
When I have one item the log is normal:
When I have two items same log is normal:
But when I have three items the log is duplicated:
Maybe the limit to last causing this issue.
Any help to fix that?
Thanks.
Assuming your getLastOrderBeta method returns an Observable you could transform it to a Promise using toPromise:
const valueAsPromise = getLastOrderBeta().toPromise()
This Promise will reject if your Observable throws anywhere and resolves with the last emitted value.
valueAsPromise
.then((value) => {
// last value from your observable
})
.catch((error) => {
// something bad happened
})
If you need to wait for multiple emitted values then you can call toArray first:
const valuesAsPromise = getLastOrderBeta().toArray().toPromise()
Which will result in a Promise which resolves with all the emitted values.
If you need custom logic to aggregate the emitted values, use reduce.

Multiple nested AJAX requests with arrays

I'm using Axios / promises to make AJAX requests, but the structure is a bit confusing. Basically I want this to happen:
Get masterlist.xml.
Parse the master list to get an array of category list URLs.
Get each category.xml.
Parse each category list to get an array of manifest URLs.
Get each manifest.xml and parse data.
Example structure:
masterlist.xml
<master>
<category>action</category>
<category>romance</category>
</master>
Category XMLs (e.g. action.xml and romance.xml)
<manifestUrls>
<manifest>foo/bar/manifest.xml</manifest>
<manifest>alice/bob/manifest.xml</manifest>
<manifest>hello/world/manifest.xml</manifest>
</manifestUrls>
Manifest XMLs
<manifest>
<data>Blah blah blah</data>
<manifest>
I'm a bit stuck on how to structure this as an Axios request with then. I'd like to have three functions, getMaster(), getCategory(url), and getManifest(url) that are preferably independent (e.g. getMaster doesn't have to call getCategory directly), but it seems like it might be necessary.
How would this be structured in Axios?
One of the main benefits of promises is that they allow you to easily avoid interdependence between your methods.
Here is a rough outline of how you could do this.
// put it all together
getMaster()
.then(parseMaster)
.then(function (categories) {
return Promise.all(categories.map(getAndParseCategory));
})
.then(flatten) // the previous then() resolves to an array-of-arrays
.then(function (manifestUrls) {
return Promise.all(manifestUrls.map(getManifest));
})
.then(function (manifests) {
// manifests is an array of all manifests
});
// Examples of what each of the functions used above would do
function getMaster() {
return axios.get('masterUrl')
.then(function (response) { return response.data; });
}
function parseMaster(masterContent) {
// parse and return an array of categories
}
function getCategory(name) {
var url = // ... build the URL based on the name
return axios.get(url)
.then(function (response) { return response.data; });
}
function parseCategory(categoryContent) {
// parse and return an array of URLs synchronously for one category
}
function getAndParseCategory(name) {
return getCategory(name).then(parseCategory);
}
function getManifest(url) {
return axios.get(url)
.then(function (response) { return response.data; });
}
function flatten(arrayOfArrays) {
return [].concat.apply([], arrayOfArrays);
}
If you're using Bluebird or something else that gives promises a .map() method, then you can tidy that pipeline up a bit:
// using Promise.resolve() at the beginning to ensure
// the chain is based of the desired kind of promise
Promise.resolve()
.then(getMaster)
.then(parseMaster)
.map(getCategory)
.map(parseCategory)
.then(flatten) // previous line resolves to an array-of-arrays
.map(getManifest)
.then(function (manifests) {
// manifests is an array of all manifests
});
Of course, you could also define your own .map method if you don't want to import a whole third party promise library:
if (!Promise.prototype.map) {
Promise.prototype.map = function (func) {
return this.then(function (result) {
return Promise.all(result.map(func));
});
};
}
Edit: To respond to your question in the comments below. If you wanted to pass the category text along so that it could be included in the manifest URLs, I think a clean way to do this would be to include that in the data returned from getCategory() so that parseCategory can make use of it. Everything else could stay the same.
Example:
function getCategory(name) {
var url = // ... build the URL based on the name
return axios.get(url)
.then(function (response) {
return {
name: name,
data: response.data
};
});
}
function parseCategory(categoryContent) {
var urls = // parse URLs from categoryContent.data
return urls.map(function (url) {
return categoryContent.name + '/' + url;
});
}

Categories

Resources