How do I do something like $q.all
but limiting how many promises are executed concurrently?
My question is just like How can I limit Q promise concurrency?
I want no more than 5 process spawned at a time
The accepted answer for that other question is a library written for promise wrapped to work with Q.
But I'm interested specifically in a solution for Angular's $q
rather than for Q
.
Background: The problem being solved:
I have a bunch of files to download in 2 steps: a) Get URL b) download file.
The browser limits how many files can be retrieved concurrently, so when the straightforward use of promises with $q.all
fires off all the downloads, only N happen right away, e.g. 6 in Chrome, while the rest are delayed. (see Max parallel http connections in a browser?)
Problem is that the URLs have expiry, so by the time the browser executes the N+1th file download, the URL is no longer valid.
So I want to do something like throttled.all(6, promises)
rather than $q.all(promise)
If the resolving of 'all' Promises is irrelevant (like you are updating some Elements on the page and you don't care if it's progressive and not in one 'whoosh') I have created a simple TaskQueue Service. It will not wait for all Promises to be resolved but rather it will process all added Promises/Functions/Tasks it gets and with a max. concurrency of a configured limit value.
As I only found this and some other StackOverflows not helping with my problem I had. So I now give something back to the community I guess. Hope it helps somebody.
It uses modern JS stuff like const and lambda expressions, but you may simply let it compile down from a precompiler like babel if you just want the 'old' stuff.
https://gist.github.com/Blackskyliner/8b1bafed326044fa4f8b1ba2627d1117
It just simply processes its queue after Tasks, which are just anonymous functions, returning a promise or value, are added. It will adhere to a configurable 'maxConcurrentTasks' variable on the service.
If the Task returns a promise which returns a promise and so on it will always use the initial 'slot' within the queue. So it will free the 'slot' for an other added Task after the whole Task Promise chain is resolved (or rejected).
I think you could do something recursive.
This would allow you to use just $q
. You would just have to have a list you would be using, which you need anyway for $q.all
self.MakeCall = function(theList, chunkSize){
//Split list into what will be processed and what should be held
var processList = theList.slice(0,chunkSize);
var holdList = theList.slice(chunkSize);
return $q.all(processList).then(function(result) {
//If holdList is empty you are finished
if(holdList <= 0) {
return result;
}
else {
//More items make recursive call to process next set
return self.MakeCall(holdList, chunkSize);
}
});
};
//Initialize Recursive call
self.MakeCall(self.contacts);
Here is an alternate solution that will run concurrent Chunks. It is not quite as clean as the other answer, but it is closer to what you are looking for.
I left some markup used for validation.
self.list [...];
self.MakeCall = function(theList, chunkSize,methodTracker){
//Set default chunkSize
chunkSize = (chunkSize)?chunkSize:1;
//Slice list using previously defined list
// so multiple instances can be runuse same list
var processList = self.list.slice(0,chunkSize);
self.list = self.list.slice(chunkSize);
return $q.all(processList).then(function(result) {
//If holdList is empty you are finished
if(self.list <= 0) {
console.debug("method last: " + methodTracker);
return result;
}
else {
//More items make recursive call to process next set
console.debug("method: " + methodTracker);
return self.MakeCall(self.list, chunkSize,methodTracker);
}
});
};
//Initialize Recursive call
self.MakeCall(self.list,1,1).then(function() {
console.warn('end 1');
});
//Initialize Second call that will run asynchronous with the first
// this can be initialized as many times as you want concurrent threads
self.MakeCall(self.list,1,2).then(function() {
console.warn('end 2');
});
I opted to put in another answer. I think the two are different enough that I didn't want to make changes to the other
I tried a few different methods to do this but in the end settled on writing an extension to $q
that extends its $q.all
behaviour with $q.allLimit()
.
See the angular-q-limit module for more details.
The module works by creating a wrapping function which takes an array of Promises and executes them in the ordered sequence (limiting the concurrent execution to the limit) until all are exhausted. It then resolves its own promise with the return values of all in the same way as $q.all()
.