Strange infinite recursion behavior with Promises - javascript

I created a NodeJS program (with Bluebird as Promise library) that handles some validations similar to how the snippet below works, but if I run that script it throws the following error:
Unhandled rejection RangeError: Maximum call stack size exceeded
Apparently, it's doing some recursive function call at the reassigning of the validations functions where I used .bind(ctx)
The way I solved that problem was assigning the Promise factory to obj._validate instead of reassigning obj.validate and use _validate(ctx) where it's needed.
But I still don't realize why that error happened. Can someone explain to me?
// Example validation function
function validate(pass, fail) {
const ctx = this
Promise.resolve(ctx.value) // Simulate some async validation
.then((value) => {
if (value === 'pass') pass()
if (value == 'fail') fail('Validation failed!')
})
}
let validations = [
{name: 'foo', validate: validate},
{name: 'bar', validate: validate},
{name: 'baz', validate: validate},
{name: 'qux', validate: validate}
]
// Reassigning validate functions to a promise factory
// to handle async validation
validations.forEach(obj => {
obj.validate = (ctx) => { // ctx used as context to validation
return new Promise(obj.validate.bind(ctx))
}
})
function executeValidations(receivedValues, validations) {
receivedValues.forEach((obj, i) => {
validations[i].validate(obj) // obj becomes the context to validate
.then(() => console.log('Validation on', obj.name, 'passed'))
.catch(e => console.error('Validation error on', obj.name, ':', e))
})
}
let receivedValues1 = [
{name: 'foo', value: 'pass'},
{name: 'bar', value: 'fail'},
{name: 'baz', value: 'fail'},
{name: 'qux', value: 'pass'},
]
executeValidations(receivedValues1, validations)
let receivedValues2 = [
{name: 'foo', value: 'pass'},
{name: 'bar', value: 'pass'},
{name: 'baz', value: 'fail'},
{name: 'qux', value: 'fail'},
]
executeValidations(receivedValues2, validations)
<script src="//cdn.jsdelivr.net/bluebird/3.4.7/bluebird.js"></script>
EDIT: I think this is a short version of the problem
function fn(res, rej) { return this.foo }
fn = function(ctx) { return new Promise(fn.bind(ctx))}
const ctx = {foo: 'bar'}
fn(ctx)
.then(console.log)
<script src="//cdn.jsdelivr.net/bluebird/3.4.7/bluebird.js"></script>

obj.validate.bind(ctx)
evaluates to an exotic function object with its this value set to ctx. It is still, very much, a function object.
It then appears that
obj.validate = (ctx) => { // ctx used as context to validation
return new Promise(obj.validate.bind(ctx))
sets obj.validate to a function which returns a promise which during its construction synchronously calls its resolver function obj.validate.bind(ctx) (a.k.a. "executor function" in ES6) which returns a promise object whose construction synchronously calls obj.validate.bind(ctx), and so on ad infinitum or the JavaScript engine throws an error.
Hence calling obj.validate a first time initiates an infinite loop of promise prodution by the resolver function.
A further issue with bind usage:
Arrow functions bind their lexical this value when declared. Syntactically Function.prototype.bind can be applied to an arrow function but does not change the this value seen by the arrow function!
Hence obj.validate.bind(ctx) never updates the this value seen within obj.validate if the method was defined using an arrow function.
Edit:
The biggest problem may be overwriting the value of the function which performs the operation:
As posted:
validations.forEach(obj => {
obj.validate = (ctx) => { // ctx used as context to validation
return new Promise(obj.validate.bind(ctx))
}
overwrites the validate property of each validations entry. This property used to be the named function validate declared at the start, but is no longer.
In the short version,
function fn(res, rej) { return this.foo }
fn = function(ctx) { return new Promise(fn.bind(ctx))}
const ctx = {foo: 'bar'}
fn(ctx)
fn = function... overwrites the named function declaration of fn. This means that when fn is called later, the fn of fn.bind(ctx) refers to the updated version of fn, not the original.
Note also that the resolver function must call its first function parameter (resolve) to synchronously resolve a new promise. Return values of the resolver function are ignored.

executeValidations() expects validate() to return a promise, so its better to return a promise. Rejecting a promise is useful when something goes wrong during a validation, but failing a validation test is a normal part of the validation process, not an error.
// Example validation function
function validate(ctx) {
return new Promise((resolve, reject) => {
// Perform validation asynchronously to fake some async operation
process.nextTick(() => {
// Passing validations resolve with undefined result
if (ctx.value === 'pass') resolve()
// Failing validations resolve with an error object
if (ctx.value == 'fail') resolve({
name: ctx.name,
error: 'Validation failed!'
})
// Something went wrong
reject('Error during validation')
})
})
}
Now executeValidations() can map the validations to a list of errors
function executeValidations(receivedValues, validations) {
// Call validate for each received value, wait for the promises to resolve, then filter out any undefined (i.e. success) results
return Promise.all(receivedValues.map( obj => validate(obj)))
.then(results => results.filter(o => o !== undefined))
}
The validations succeeded if there were no errors...
executeValidations(receivedValues1, validations)
.then (errors => {
if (!errors.length)
console.log('Validations passed')
else
errors.forEach(error => console.error(error))
})
executeValidations(receivedValues2, validations)
.then (errors => {
if (!errors.length)
console.log('Validations passed')
else
errors.forEach(error => console.error(error))
})

Related

how to get return value of a non-blocking function with an await on it

Lets say i have the following function:
async function commentMediaId(ig, media_id, commentContent, commentIfAlreadyCommented = false, extraInfo = new Object(), callback) {
try {
let alreadyExists = ig.db.get('comments').find({media_id: media_id}).value();
alreadyExists == undefined ? false : true;
if(alreadyExists && !commentIfAlreadyCommented) {
console.log('Already commented'.yellow);
return "already_commented";
}
if(commentIfAlreadyCommented || !alreadyExists){
await ig.media.comment({
module_name: 'profile',
mediaId: media_id,
text: commentContent,
});
return callback('success');
}
} catch (e) {
return callback('not success');
}
}
then I am calling the following function (i do not want to use await here because it will block):
commentMediaId(object, 'someid', 'some string');
how can i seet a callback on commentMediaId such that when it returns i get the value of success or not success ?
I basically wanted to do as follows
commentMediaId(object, 'someid', 'some string', function(result) {
if (result == 'success')
});
but I am getting a bunch of syntax error.. any ideas on how to implement it ?
When you use an async function, you are working with the Promise interface, which exposes a .then() method. You can pass a callback into this method, and the callback will automatically be invoked for you when a value is returned from the commentMediaId function:
commentMediaId(object, 'someid', 'some string')
.then(function (result) {
console.log(result);
})
;
Finally, you need not take in a callback parameter in the commentMediaId function. You can omit the callback and simply return the values directly. That is the whole idea of the async/await "syntactic sugar", it makes it feel more like a regular function without the need for callbacks.
E.g. the following:
return callback('success');
...should be changed to the following:
return 'success';
it looks like you have some typos - asyn should be async and you also have an extra parenthesis in the last argument to the function commentMediaId(...otherArgs, callback(){} instead of commentMediaId(...otherArgs, callback){}

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...]

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

Node.js — Maximum call stack size exceeded, even with process.nextTick()

I'm trying to write a module for "chainable" Express.js validation:
const validatePost = (req, res, next) => {
validator.validate(req.body)
.expect('name.first')
.present('The parameter is required')
.string('The parameter must be a string')
.go(next);
};
router.post('/', validatePost, (req, res, next) => {
return res.send('Validated!');
});
The code of validator.validate (simplified for brevity):
const validate = (data) => {
let validation;
const expect = (key) => {
validation.key = key;
// Here I get the actual value, but for testing purposes of .present() and
// .string() chainable methods it returns a random value from a string,
// not string and an undefined
validation.value = [ 'foo', 123, void 0 ][Math.floor(Math.random() * 3)];
return validation;
};
const present = (message) => {
if (typeof validation.value === 'undefined') {
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
return validation;
};
const string = (message) => {
if (typeof validation.value !== 'string') {
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
return validation;
};
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
// I even wrap async callbacks in process.nextTick()
process.nextTick(() => next(error));
}
process.nextTick(next);
};
validation = {
valid: true,
data: data,
errors: [],
expect: expect,
present: present,
string: string,
go: go
};
return validation;
};
The code works fine for short chains, returning a proper error object. However if I chain a lot of methods, say:
const validatePost = (req, res, next) => {
validator.validate(req.body)
.expect('name.first')
.present('The parameter is required')
.string('The parameter must be a string')
.expect('name.first') // Same for testing
.present('The parameter is required')
.string('The parameter must be a string')
// [...] 2000 times
.go(next);
};
Node.js throws RangeError: Maximum call stack size exceeded. Note that I wrapped my async callback .go(next) in a process.nextTick().
I didn't have a lot of time to look at this, but I did notice a pretty big problem. You have a single-branch if statement that leads to next being called twice when !validator.valid is true. In general, single-branch if statements are a code smell.
This might not be the reason you're experiencing a stack overflow, but it's a likely culprit.
(Code changes appear in bold)
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
process.nextTick(() => next(error));
}
else {
process.nextTick(next);
}
};
Some people use return to cheat with if too. This also works, but it sucks
const go = (next) => {
if (!validation.valid) {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
process.nextTick(() => next(error));
return; // so that the next line doesn't get called too
}
process.nextTick(next);
};
I think the entire go function is expressed better like this ...
const go = (next) => {
// `!` is hard to reason about
// place the easiest-to-understand, most-likely-to-happen case first
if (validation.valid) {
process.nextTick(next)
}
// very clear if/else branching
// there are two possible outcomes and one block of code for each
else {
let error = new Error('Validation error');
error.name = 'ValidationError';
error.errors = validation.errors;
// no need to create a closure here
process.nextTick(() => next(error));
process.nextTick(next, error);
}
};
Other remarks
You have other single-branch if statements in your code too
const present = (message) => {
if (typeof validation.value === 'undefined') {
// this branch only performs mutations and doesn't return anything
validation.valid = false;
validation.errors.push({ key: validation.key, message: message });
}
// there is no `else` branch ...
return validation;
};
This one is less offensive, but I still think it's harder to reason about once you gain an appreciation for if statements that always have an else. Consider the ternary operator (?:) that forces both branches. Also consider languages like Scheme where a True and False branch are always required when using if.
Here's how I'd write your present function
const present = (message) => {
if (validation.value === undefined) {
// True branch returns
return Object.assign(validation, {
valid: false,
errors: [...validation.errors, { key: validation.key, message }]
})
}
else {
// False branch returns
return validation
}
};
It's an opinionated remark, but I think it's one worth considering. When you have to return to this code and read it later, you'll thank me. Of course once your code is in this format, you can sugar the hell out of it to remove a lot of syntactic boilerplate
const present = message =>
validation.value === undefined
? Object.assign(validation, {
valid: false,
errors: [...validation.errors, { key: validation.key, message }]
})
: validation
Advantages
Implicit return effectively forces you to use a single expression in your function – this means you cannot (easily) over-complicate your functions
Ternary expression is an expression, not a statement – if does not have a return value so use of ternary works well with the implicit return
Ternary expression limits you to one expression per branch – again, forces you to keep your code simple
Ternary expression forces you to use both true and false branches so that you always handle both outcomes of the predicate
And yes, there's nothing stopping you from using () to combine multiple expressions into one, but the point isn't to reduce every function to a single expression – it's more of an ideal and nice to use when it works out. If at any time you feel readability was affected, you can resort to if (...) { return ... } else { return ... } for a familiar and friendly syntax/style.
Method Chaining Overflow
From your full code paste
validate({ name: { last: 'foo' }})
// Duplicate this line ~2000 times for error
.expect('name.first').present().string()
.go(console.log);
You simply cannot chain that many methods in a single expression.
In an isolated test, we show that this has nothing to do with recursion or process.nextTick
class X {
foo () {
return this
}
}
let x = new X()
x.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
...
.foo().foo().foo().foo().foo().foo().foo().foo().foo().foo()
// RangeError: Maximum call stack size exceeded
Using 64-bit Chrome on OSX, the method chaining limit is 6253 before a stack overflow happens. This likely varies per implementation.
Lateral thinking
A method-chaining DSL seems like a nice way to specify validation properties for your data. It's unlikely you'd need to chain more than a couple dozen lines in a given validation expression, so you shouldn't be too worried about the limit.
Aside from that, a completely different solution might be altogether better. One example that immediately comes to mind is JSON schema. Instead of writing validation with code, would write it declaratively with data.
Here's a quick JSON schema example
{
"title": "Example Schema",
"type": "object",
"properties": {
"firstName": {
"type": "string"
},
"lastName": {
"type": "string"
},
"age": {
"description": "Age in years",
"type": "integer",
"minimum": 0
}
},
"required": ["firstName", "lastName"]
}
There's effectively no limit on how big your schema can be, so this should be suitable to solve your problem.
Other advantages
Schema is portable so other areas of your app (eg testing) or other consumers of your data can use it
Schema is JSON so it's a familiar format and users don't need to learn new syntax or API

Combine related data with promise results

I have an array of objects that I need to make an async call
for each one, once all calls have been made I'd like to package each module up as an object containing the original object's detail, and the
new results from the async call, and put that all in an array.
Im having some trouble trying to figure how to combine the original module object with the async
result.
This is what I have so far -
const modules = [{name: 'module1', id: 1}, {name: 'module2', id: 2}, ..etc];
let modulesHistory = []; // my final array object
let modulesPromises = []; // store all my promises
modules.forEach((module) => {
const buildHistory = new BuildHistory(module.id); // my collection
// I tried to do something like this
// but it's not really doing it:
//
// modulesHistory.push({
// module: module,
// builds: buildHistory
// });
modulesPromises.push(buildHistory.fetch()); // fetch returns a promise
});
$.when.apply($, modulesPromises).done(function(){
// Afterwards I'd like modulesHistory to look like:
// [
// {
// module: {
// name: 'module1',
// id: 1
// },
// data: {
// // stuff returned from asyc
// }
// },
// {
// module: {
// name: 'module2',
// id: 2
// },
// data: {
// // stuff returned from asyc
// }
// }
// ]
doStuff(modulesHistory);
});
Augment the result of a call to buildHistory#fetch by wrapping it in a new Promise and manually resolving to an object that takes the current module and the data returned from buildHistory#fetch:
let modulesPromises = modules.map(module => {
const buildHistory = new BuildHistory(module.id)
return new Promise(res => buildHistory.fetch().then(data => res({module, data}))
})
Or, as #torazaburo suggests, we can compact this functionality further by avoiding creating a new Promise:
let modulesPromises = modules.map(module => {
return new BuildHistory(module.id)
.fetch()
.then(data => ({module, data}))
})
Indeed, as #Bergi has mentioned in a comment, you should consider using the native implementation of Promises (that is available in ES6) to access the final result:
Promise.all(modulePromises).then(res => {
// res is an array of the result of each Promise in `modulePromises`
})

Categories

Resources