Managing the state of multiple concurrent requests and then syncing the results can be quite some work.
Managing state is one of the main purposes of Promises, and Promise.all is syncing and merging the results.
That's also the main purpose of the following code. Two things left to say:
this code ain't tested, it may contain some errors
I've commented pretty much everything in this code four you to understand what its purpose is/mechanics are, and what it is capable of, and how to approach different use cases for this monster. That's why this answer ended up so darn long.
Since the actual code to load a single file was so short and isolated, I decided to put that into an external function so you can reuse this whole code by only passing a different utility-function to do the actual request.
And because I prefer named mappings over plain arrays accessed by index (it's easier to not confuse names than indices), I've integrated this possibility too. If you don't know exactly what I mean by that, take a look at the examples after the main function.
And as additional sugar, and since it took only a minor tweak, I've made the returned function recursive, so it can deal with pretty much everything you pass to it as a "list" of urls.
function loadFilesFactory( loadFile ){
function isNull(value){ return value === null }
//takes an array of keys and an array of values and returns an object mapping keys to values.
function zip(keys, values){
return keys.reduce(function(acc, key, index){
acc[key] = values[index];
return acc;
}, Object.create(null)); //if possible
//}, {}); //if Object.create() doesn't work on the browser you need to support
}
//a recursive function that can take pretty much any composition as "url"
//and will "resolve" to a similar composition of results,
//while loading everything in paralell
//see the examples
var recursiveLoadFilesFunction = function(arg, callback){
if(arg !== Object(arg)){
//arg is a primitive
return loadFile(arg, callback);
}
if(!Array.isArray(arg)){
//arg is an object
var keys = Object.keys(arg);
return recursiveLoadFilesFunction(keys.map(function(key){
return arg[key];
}), function(error, values){
if(error) callback(error)
else callback(null, zip(keys, values));
});
}
//arg contains an array
var length = arg.length;
var pending = Array(length)
var values = Array(length);
//If there is no request-array anymore,
//then some (sync) request has already finished and thrown an error
//no need to proceed
for(var i = 0; pending && i<length; ++i){
//I'd prefer if I'd get the request-object to be able to abort this, in case I'd need to
pending[i] = recursiveLoadFilesFunction(
arg[i],
createCallbackFor(i)
//but if I don't get a sufficient value, I need at least to make sure that this is not null/undefined
) || true;
}
var createCallbackFor = function(index){
return function(error, data){
//I'm done, this shouldn't have been called anymore
if(!pending || pending[index] === null) return;
//this request is done, don't need this request-object anymore
pending[index] = null;
if(error){
//if there is an error, I'll terminate early
//the assumption is, that all these requests are needed
//to perform, whatever the reason was you've requested all these files.
abort();
values = null;
}else{
//save this result
values[index] = data;
}
if(error || pending.every( isNull )){
pending = null; //says "I'm done"
callback(err, values);
}
}
}
var abort = function(){
if(pending){
//abort all pending requests
pending.forEach(function(request){
if(request && typeof request.abort === "function")
request.abort();
});
//cleanup
pending = null;
}
}
return {
//providing the ability to abort this whole batch.
//either manually, or recursive
abort: abort
}
}
return recursiveLoadFilesFunction;
}
This is the only part, that would change if you'd want to reuse this whole thing for let's say JSON files, or a different csv-formatting, or whatever
var loadCsvFiles = loadFilesFactory(function(url, callback){
if(!url || typeof url !== "string"){
callback(JSON.stringify(url) + ' is no valid url');
return;
}
return d3.csv(url, function(d){ ... }, callback);
});
what can this code handle?
//plain urls, sure
loadCsvFiles('url', function(err, result){ ... })
//an array of urls, it's inital purpose
loadCsvFiles(['url1', 'url2', 'url3'], function(err, results){
console.log(results[0], results[1], results[2]);
});
//urls mapped by property names, I've already mentioned that I prefer that over array indices
loadCsvFiles({
foo: 'file1.csv',
bar: 'file2.csv'
}, function(err, results){
//where `results` resembles the structure of the passed mapping
console.log(results.foo, results.bar);
})
//and through the recursive implementation,
//pretty much every imaginable (non-circular) composition of the examples before
//that's where it gets really crazy/nice
loadCsvFiles({
//mapping a key to a single url (and therefoere result)
data: 'data.csv',
//or one key to an array of results
people: ['people1.csv', 'people2.csv'],
//or a key to a sub-structure
clients: {
jim: 'clients/jim.csv',
//no matter how many levels deep
joe: {
sr: 'clients/joe.sr.csv',
jr: 'clients/joe.jr.csv',
},
//again arrays
harry: [
'clients/harry.part1.csv',
'clients/harry.part2.csv',
//and nested arrays are also possible
[
'clients/harry.part3a.csv',
'clients/harry.part3b.csv'
]
]
},
//of course you can also add objects to Arrays
images: [
{
thumbs: 'thumbs1.csv',
full: 'full1.csv'
},
{
thumbs: 'thumbs2.csv',
full: 'full2.csv'
}
]
}, function(err, results){
//guess what you can access on the results object:
console.log(
results.data,
results.people[0],
results.people[1],
results.clients.jim,
results.clients.joe.sr,
results.clients.joe.jr,
results.clients.harry[0],
results.clients.harry[1],
results.clients.harry[2][0],
results.clients.harry[2][1],
results.images[0].thumbs,
results.images[0].full,
results.images[1].thumbs,
results.images[1].full
)
});
Especially this last example may not make any sense to you, in terms of an absurd structure for csv-files, but that's not the point. The point is, that it is completely up to you how you structure your data. Just pass it to this file loader and it will handle that.
And if you want this to support multiple file formats at once, it is also possible with a simple tweak:
var loadDifferentFiles = loadFilesFactory(function(url, callback){
if(!url || typeof url !== "string"){
callback(JSON.stringify(url) + ' is no valid url');
return;
}
if(url.endsWith('.csv')){
return d3.csv(url, callback);
}
if(url.endsWith('.json')){
return d3.json(url, callback);
}
//return d3.text(url, callback);
callback('unsupported filetype: ' + JSON.stringify(url));
});
or sth. like this
var loadDifferentFiles = loadFilesFactory(function(value, callback){
if(typeof value !== "string"){
if(value.endsWith('.csv')){
return d3.csv(value, callback);
}
if(value.endsWith('.json')){
return d3.json(value, callback);
}
}
//but in this case, if I don't know how to handle a value
//instead of resolving to an error, just forwarding the passed value to the callback,
//implying that it probably wasn't meant for this code.
callback(null, value);
});