I have the following data structure for a tree of comments in a thread. This structure is contained inside a single object.
comment {
id: 1,
text: 'foo',
children: [
comment {
id: 2,
text: 'foo-child',
children: []
},
comment {
id: 3,
text: 'foo-child-2',
children: []
}
]
},
comment {
id: 4,
text: 'bar',
children: []
}
This is provided by a back-end API, there's no problem in that. What I want to do is recursively explore this tree and for each node (either root or child node) I want to perform an API call and get some extra data for every single node, slap in some extra properties, and return the entire tree with the new keys added to each node.
function expandVoteData(comments) {
return new Promise((resolve, reject) => {
let isAuth = Auth.isUserAuthenticated();
// 'this' is the vote collection
async.each(comments, (root, callback) => {
// First get the vote data
async.parallel({
votedata: function(callback) {
axios.get('/api/comment/'+root.id+'/votes').then(votedata => {
callback(null, votedata.data);
});
},
uservote: function(callback) {
if(!isAuth) {
callback(null, undefined);
} else {
axios.get('/api/votes/comment/'+root.id+'/'+Auth.getToken(), { headers: Auth.getApiAuthHeader() }).then(uservote => {
callback(null, uservote.data); // Continue
});
}
}
}, function(error, data) {
if(error) {
console.log('Error! ', error);
} else {
// We got the uservote and the votedata for this root comment, now expand the object
root.canVote = isAuth;
root.totalVotes = data.votedata.total;
root.instance = 'comment';
if(data.uservote !== undefined) {
root.userVote = data.uservote;
}
if(root.children && root.children.length > 0) {
// Call this function again on this set of children
// How to "wrap up" this result into the current tree?
expandVoteData(root.children);
}
callback(); // Mark this iteration as complete
}
});
}, () => {
// Done iterating
console.log(comments);
resolve();
});
})
}
What it does is: accept a 'comments' parameter (which is the entire tree object), create a promise, iterate through each leaf node, and perform the respective API calls in asynchronous requests. If the leaf node has any children, repeat the function with each child node.
This theoretically would work perfectly in a synchronous world, but what I do need to do is to get the new tree after every node has been processed for further processing, as a single object, just like it was as an input. In fact, I get multiple console prints for each individual node in the tree, evidencing that the code works as it's written... I don't want individual prints though, I want to wrap up the entire set of results in a single object. Ideally, the function should be called like this:
expandVoteData(comments).then(expanded => {
// yay!
});
Any tips on how to perform this? Thank you beforehand.
requests in serial
Below,
addExtra
accepts an inputcomment
and asynchronously adds additional fields to the comment, and all of the comment'schildren
recursively.To show this works, we first introduce a fake database. We can query a comment's extra fields by comment
id
Now instead of
axios.get
we use ourfetchExtra
. We can seeaddExtra
works as intended, given the first comment as inputSince you have an array of comments, we can use
map
toaddExtra
to eachUsing
Promise.all
is a burden on the user though, so it'd be nice to have something likeaddExtraAll
refactor and enlighten
Did you notice a code duplication? Hello, mutual recursion...
Verify the results in your own browser below
add multiple fields
Above,
addExtra
is simple and only adds oneextra
field to your comment. We could add any number of fieldsmerging results
Instead of adding fields to the
comment
, it's also possible to merge the fetched data in. However, you should take some precautions here ...Note the order of the calls above. Because we call
...await
first, it's impossible for fetched data to overwrite fields in your comment. For example, iffetchExtra(1)
returned{ a: 1, id: null }
, we would still end up with comment{ id: 1 ... }
. If you wish to have the possibility for added fields to overwrite existing fields in your comment, then you can change the orderingAnd lastly, you can make multiple merges if you wanted
requests in parallel
One disadvantage of the approach above is that requests for the extra fields are done in serial order.
It would be nice if we could specify a a function that takes our comment as input and returns an object of the fields we wish to add. This time we skip the
await
keywords so that our function can parallelize the subrequests for us automaticallyHere's one way to implement
addFieldsAll
. Also note, because of the ordering of the arguments toObject.assign
it is possible for the descriptor to specify fields that would overwrite fields on the input comment – egc => ({ id: regenerateId (c.id), ... })
. As described above, this behavior can be changed by reordering the arguments as desiredIt gets more easy if you seperate the code into multiple functions and use the cool
async
/await
syntax. Furst define an async function that updates one node without caring for the children:To update all nodes recursively its then as easy as: