I've got a complicated (at least for me) set up of nested loops, ajax calls, and deferreds. The code is calling an API, parsing out relevant data, then using it to make further calls to other APIs.
It works almost as intended. I used the answer to this question (Using $.Deferred() with nested ajax calls in a loop) to build it. Here's my code:
function a() {
var def = $.Deferred();
var req = [];
for (var i = 0 /*...*/) {
for (var j = 0 /*...*/) {
(function(i, j) {
req.push($.ajax({
//params
}).done(function(resp) {
var def2 = $.Deferred();
var req2 = [];
for (var k = 0 /*...*/) {
for (var l = 0 /*...*/) {
req2.push(b(l));
}
}
$.when.apply($, req2).done(function() {
console.log("Got all data pieces");
def2.resolve();
})
}));
})(i, j);
}
}
$.when.apply($, req).done(function() {
console.log("Got all data");
def.resolve();
});
return def.promise();
}
function b(j) {
var def = $.Deferred();
$.when.apply(
$.ajax({
//params
})
).then(function() {
console.log("Got data piece #" + l);
def.resolve();
});
return def.promise();
}
function main() {
//...
$.when.apply($, a()).then(function() {
console.log("All done");
displayPage();
})
//...
}
Here's what I'm expecting to see when the calls complete
(In no specific order)
Got data piece #1
Got data piece #0
Got data piece #2
Got all data pieces
Got data piece #2
Got data piece #1
Got data piece #0
Got all data pieces
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces
Got all data <-- These two must be last, and in this order
All done
Here's what I'm seeing
All done
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces
Got data piece #0
Got data piece #1
Got data piece #2
Got all data pieces
I stepped through it in the debugger, and the 'Got all data' line in function a() gets printed in the correct sequence after everything else completes, after which def.resolve() should get called and resolve the returned promise.
However, in main(), a() is seen as resolved right away and the code jumps right into printing 'All done' and displaying the page. Any ideas as to why it doesn't wait as it's supposed to?
You have illustrated a set of code and said it isn't doing what you expected, but you haven't really described the overall problem. So, I don't actually know exactly what code to recommend. We do a lot better here with real problems rather than pseudo code problems. So, instead, what I can do is to outline a bunch of things that are wrong with your code:
Expecting serial order of parallel async operations
Based on what you say you are expecting, the basic logic for how you control your async operations seems to be missing. When you use $.when() on a series of promises that have already been started, you are running a whole bunch of async operations in parallel. Their completion order is completely unpredictable.
Yes, you seem to expect to be able to run a whole bunch of b(i) in parallel and have them all complete in order. That seems to be the case because you say you are expecting this type of output:
Got data piece #0
Got data piece #1
Got data piece #2
where each of those statements is generated by the completion of some b(i) operation.
That simply will not happen (or it would be blind luck if it did in the real world because there is no code that guarantees the order). Now, you can run them in parallel and use $.when() to track them and $.when() will let you know when they are all done and will collect all the results in order. But when each individual async operation in that group finishes is up to chance.
So, if you really wanted each of your b(i) operations to run and complete in order, then you would have to purposely sequence them (run one, wait for it to complete, then run the next, etc...). In general, if one operation does not depend upon the other, it is better to run them in parallel and let $.when() track them all and order the results for you (because you usually get your end result faster by running them all in parallel rather than sequencing them).
Creation of unnecessary deferreds in lots of places - promse anti-pattern
In this code, there is no need to create a deferred at all. $.ajax() already returns a promise. You can just use that promise. So, instead of this:
function b(j) {
var def = $.Deferred();
$.when.apply(
$.ajax({
//params
})
).then(function() {
console.log("Got data piece #" + l);
def.resolve();
});
return def.promise();
}
You can do this:
function b(j) {
return $.ajax({
//params
}).then(function(data) {
console.log("Got data piece #" + l);
return data;
});
}
Note, that you just directly return the promise that is already produced by $.ajax() and no deferred needs to be created at all. This is also a lot more bulletproof for error handling. One of the reason your method is called an anti-pattern is you don't handle errors at all (a common mistake when using this anti-pattern). But, the improved code, propagates errors right back to the caller just like they should be. In your version, if the $.ajax() call rejects its promise (due to an error), your deferred is NEVER resolved and the caller never sees the error either. Now, you could write extra code to handle the error, but there is no reason to. Just return the promise you already have. When coding with async operations that return promises, you should pretty much never need to create your own deferred.
$.when() is only needed when you have more than one promise
In your b() function, there is no need to use $.when() in this piece of code:
$.when(
$.ajax({
//params
})).then(...);
When you have a single promise, you just use .then() directly on it.
$.ajax({
//params
}).then(...);
Only use $.when() when you have more than one promise and you want to know when all of them are done. If you only have one promise, just use its own .then() handler.
More anti-pattern - just return promises from .then() handlers
In your inner loop, you have this:
$.when.apply($, req2).done(function() {
console.log("Got all data pieces");
def2.resolve();
})
There are several things wrong here. It's not clear what you're trying to do because def2 is a deferred that nothing else uses. So, it appears you're trying to tell someone when this req2 group of promises is done, but nobody is using it. In addition it's another version of the anti-pattern. $.when() already returns a promise. You don't need to create a deferred to resolve when $.when() completes. You can just use the promise that $.when() already returns.
Though I don't fully know your intent here, it appears that what you should probably do is to get rid of the def2 deferred entirely and do just this:
return $.when.apply($, req2).done(function() {
console.log("Got all data pieces");
});
Returning this promise from the .then() handler that it is within will chain this sequence of actions to the parent promise and make the parent promise wait for this new promise to be resolved (which is tied to when all the req2 promises are done) before the parent promise will resolve. This is how you make parent promises dependent upon other promise within a .then() handler. You return a promise from the .then() handler.
And, the exact same issue is true for your outer $.when.apply($, req) also. You don't need a deferred there at all. Just use the promise that $.when() already returns.
Putting it together
Here's a cleaned up version of your code that gets rid of the anti-patterns in multiple places. This does not change the sequencing of the b(i) calls among themselves. If you care about that, it is a bigger change and we need to see more of the real/actual problem to know what best to recommend.
function a() {
var req = [];
for (var i = 0 /*...*/) {
for (var j = 0 /*...*/) {
(function(i, j) {
req.push($.ajax({
//params
}).then(function(resp) {
var req2 = [];
for (var k = 0 /*...*/) {
for (var l = 0 /*...*/) {
req2.push(b(l));
}
}
return $.when.apply($, req2).done(function() {
console.log("Got all data pieces");
});
}));
})(i, j);
}
}
return $.when.apply($, req).done(function() {
console.log("Got all data");
});
}
function b(j) {
return $.ajax({
//params
}).then(function(data) {
console.log("Got data piece #" + l);
return data;
});
}
function main() {
//...
a().then(function() {
console.log("All done");
displayPage();
});
//...
}
P.S. If you want to process the b(i) results from within the same group in order, then don't use a .then() handler on the individual promise because those will execute in arbitrary order. Instead, use the results that come with $.when().then(result1, result2, ...) and process them all there. Though the individual promises complete in an arbitrary order, $.when() will collect the results into the original order so if you process the results in the $.when() handler, then you can process them all in order.
Related
I use jQuery. And I don't want parallel AJAX calls on my application, each call must wait the previous before starting. How to implement it? There is any helper?
UPDATE If there is any synchronous version of the XMLHttpRequest or jQuery.post I would like to know. But sequential != synchronous, and I would like an asynchronous and sequential solution.
There's a much better way to do this than using synchronous ajax calls. Jquery ajax returns a deferred so you can just use pipe chaining to make sure that each ajax call finishes before the next runs. Here's a working example with a more in depth example you can play with on jsfiddle.
// How to force async functions to execute sequentially
// by using deferred pipe chaining.
// The master deferred.
var dfd = $.Deferred(), // Master deferred
dfdNext = dfd; // Next deferred in the chain
x = 0, // Loop index
values = [],
// Simulates $.ajax, but with predictable behaviour.
// You only need to understand that higher 'value' param
// will finish earlier.
simulateAjax = function (value) {
var dfdAjax = $.Deferred();
setTimeout(
function () {
dfdAjax.resolve(value);
},
1000 - (value * 100)
);
return dfdAjax.promise();
},
// This would be a user function that makes an ajax request.
// In normal code you'd be using $.ajax instead of simulateAjax.
requestAjax = function (value) {
return simulateAjax(value);
};
// Start the pipe chain. You should be able to do
// this anywhere in the program, even
// at the end,and it should still give the same results.
dfd.resolve();
// Deferred pipe chaining.
// What you want to note here is that an new
// ajax call will not start until the previous
// ajax call is completely finished.
for (x = 1; x <= 4; x++) {
values.push(x);
dfdNext = dfdNext.pipe(function () {
var value = values.shift();
return requestAjax(value).
done(function(response) {
// Process the response here.
});
});
}
Some people have commented they have no clue what the code does. In order to understand it, you first need to understand javascript promises. I am pretty sure promises are soon to be a native javascript language feature, so that should give you a good incentive to learn.
You have two choices that I can think of. One is to chain them through callbacks. The other is to make the calls synchronous rather than async.
Is there a reason you want them sequential? That will slow things down.
To make the call synchronous, you'll set the async option in the Ajax call to false. See the documentation at http://docs.jquery.com/Ajax/jQuery.ajax#options (click options tab to see them).
(async () => {
for(f of ['1.json','2.json','3.json']){
var json = await $.getJSON(f);
console.log(json)
};
})()
requests 3 json files with jQuery ajax calls
process in sequence (not in parallel) with await
works in Chrome/Firefox/Edge (as of 1/30/2018)
more at MDN
The best way you could do this is by chaining callbacks as Nosredna said. I wouldn't recommend using synchronous XMLHttpRequest as they lock your entire application.
There aren't much helper for this as far as I know, but you could do something resembling a callback FIFO.
You could give narrative javascript a try http://www.neilmix.com/narrativejs/doc/
I've never used it myself though. If I wanted to do this, I would setup some kind of abstraction for chaining asynchronous actions. As others have said, the synchonous version of the ajax object blocks events from being processed while it's waiting for a response. This causes the browser to look like it's frozen until it recieves a response.
Set the async option to false, e.g.,
$.ajax({ async: false /*, your_other_ajax_options_here */ });
Reference: Ajax/jQuery.ajax
You can use promise to make ajax calls sequential. Using Array push and pop promise method, sequential ajax calls will be lot easier.
var promises = [Promise.resolve()];
function methodThatReturnsAPromise(id) {
return new Promise((resolve, reject) => {
$.ajax({
url: 'https://jsonplaceholder.typicode.com/todos/'+id,
dataType:'json',
success: function(data)
{
console.log("Ajax Request Id"+id);
console.log(data);
resolve();
}
});
});
}
function pushPromise(id)
{
promises.push(promises.pop().then(function(){
return methodThatReturnsAPromise(id)}));
}
pushPromise(1);
pushPromise(3);
pushPromise(2);
Look at this: http://docs.jquery.com/Ajax/jQuery.ajax (click on the "options" tab).
But remember a synchronous call will freeze the page until the response is received, so it can't be used in a production site, because users will get mad if for any reason they have to wait 30 seconds with their browser frozen.
EDIT: ok, with your update it's clearer what you want to achieve ;)
So, your code may look like this:
$.getJSON("http://example.com/jsoncall", function(data) {
process(data);
$.getJSON("http://example.com/jsoncall2", function (data) {
processAgain(data);
$.getJSON("http://example.com/anotherjsoncall", function(data) {
processAgainAndAgain(data);
});
});
});
This way, the second call will only be issued when the response to the first call has been received and processed, and the third call will only be issued when the response to the second call has been received and processed. This code is for getJSON but it can be adapted to $.ajax.
The modern way of sequencing jQuery asynchronous operations is to use the promises they already return and the flow control that promises support and this is not currently shown in any of the other answers here from prior years.
For example, let's suppose you wanted to load several scripts with $.getScript(), but the scripts must be loaded sequentially so the second one doesn't load/run until the first has finished and so on and you want to know when they are all done. You can directly use the promise that $.getScript() already returns. For simplicity, you can await that promise in a for loop like this:
async function loadScripts(scriptsToLoad) {
for (const src of scriptsToLoad) {
await $.getScript(src);
}
}
loadScripts([url1, url2, url3]).then(() => {
console.log("all done loading scripts");
}).catch(err => {
console.log(err);
});
Since all jQuery Ajax-related asynchronous operations now return promises (and have for many years now), you can extend this concept to any of jQuery's Ajax-related operations.
Also, note that all the other attempts in other answers here to wrap a jQuery operation in a new promise or in a jQuery deferred are obsolete and considered a promise anti-pattern because when the operation itself already returns a promise, you can just use that promise directly without trying to wrap it in your own new promise.
Synchronous calls aren't necessarily slower, if you have an app where AJAX calls open, posts to, then closes a socket, multiple calls to the socket don't make sense as some sockets can only handle a single connection, in which case, queuing data so its only sent when the previous AJAX call has completed means much higher data throughput.
How about using Node.js events?
var EventEmitter = require('events').EventEmitter;
var eventEmitter = new EventEmitter();
var $ = require('jquery');
var doSomething = function (responseData) {
var nextRequestData = {};
// do something with responseData
return nextRequestData;
};
// ajax requests
var request1 = $.ajax;
var request2 = $.ajax;
var requests = [request1, request2];
eventEmitter.on('next', function (i, requestData) {
requests[i](requestData).then(
function (responseData) {
console.log(i, 'request completed');
if (i+1 < requests.length) {
var nextRequestData = doSomething(responseData);
eventEmitter.emit('next', i+1, nextRequestData);
}
else {
console.log('completed all requests');
}
},
function () {
console.log(i, 'request failed');
}
);
});
var data = {
//data to send with request 1
};
eventEmitter.emit('next', 0, data);
sequential != synchronous, and I would like an asynchronous and sequential solution
Synchronous execution generally means "using the same clock", while sequential execution means "following in order or sequence".
For your specific use case I think both conditions must be met, as asynchronous execution implies the possibility of a non-sequential result.
I'm making either 1 or more REST/ajax calls to validate some user information. The rest calls are working well and the information is coming back. The issue I'm facing isn't with that part of the code, which looks something like this.
function ensureUsers(recipients){
var promises = [];
for(var key in recipients){
var payload = {'property':recipients[key]};
promises.push( $.ajax({...}));
}
return $.when.apply($,promises);
}
....
ensureUsers(users) // users is an array of 1 or more users
.done(function(){
console.log(arguments);
)}
If there is more than one user in the initial array, then the arguments in my .done code are structured like this:
[[Object,"success",Object],[Object,"success",Object]...]
I can then iterate over each result, check the status, and proceed.
However if there is only one user in the initial array then .done gets arguments like this:
[Object,"success",Object]
It seems strange to me that the structure of what is returned would change like that. I couldn't find anything about this specific a problem, so I hacked together a solution
var promises = Array.prototype.slice.call(arguments);
if(!Array.isArray(promises[0])){
promises = [promises];
}
Is that really the best I can hope for? Or is there some better way to deal with the returned promises from 1 or more ajax calls in jQuery?
It seems strange to me that the structure of what is returned would change like that.
Yes, jQuery is horribly inconsistent here. When you pass a single argument to $.when, it tries to cast it to a promise, when you pass multiple ones it suddenly tries to wait for all of them and combine their results. Now throw in that jQuery promises can resolve with multiple values (arguments), and add a special case for that.
So there are two solutions I could recommend:
Drop $.when completely and just use Promise.all instead of it:
var promises = [];
for (var p of recipients) {
…
promises.push( $.ajax({…}));
}
Promise.all(promises)
.then(function(results) {
console.log(results);
})
Make each promise resolve with only a single value (unlike $.ajax() that resolves with 3) so that they don't get wrapped in an array, and $.when will produce consistent results regardless of number of arguments:
var promises = [];
for (var p of recipients) {
…
promises.push( $.ajax({…}).then(function(data, textStatus, jqXHR) {
return data;
}) );
}
$.when.apply($, promises)
.then(function() {
console.log(arguments);
})
It appears this functionality is working as currently documented. When you have multiple deferreds passed to $.when it creates a new Promise object that is resolved with the results of each of the results of the passed in deferreds. When you only pass in one, it returns the deferred that you passed in, thus only returning the result instead of an array of results.
I'm not sure if it is any better than your current solution, but you could force it to always have multiple deferreds by having a "fake" deferred that you skip when evaluating the results.
I.E.
function ensureUsers(recipients){
var promises = [$.Deferred().resolve()];
for(var key in recipients){
var payload = {'property':recipients[key]};
promises.push( $.ajax({...}));
}
return $.when.apply($,promises);
}
You could also potentially make it so the placeholder deferred is resolved with the same structure as what you expect in your real results so it would just appear that the first response is always a success.
I have a function which creates a database object out of three arrays. The arrays are filled in an each loop, one of the arrays relies on the value in the same iteration of the loop.
The dependent array uses the requests library and the cheerio library to grab a string to populate the array with.
Currently the dependent array fills with nulls which I think is because the loop is not waiting for the request to be returned.
I am still learning and would like to get this to work without direct blocking to keep things asynchronous so I'm looking into promises/callbacks.
This is being done server-side but from what I've seen in cheerios docs there is no promises capability.
Here's what I have so far. (getFile() is the function that isn't filling the 'c' array, it also depends on the current value being put into 'b'). I do know that the getFile function gets the correct value with a console log test, so the issue must be in the implementation of filling 'c'.
addToDB() is a function which saves a value into mongoDB, from testing I know that the objects are correctly being put into the db, just the c array is not correct.
function getInfo(path) {
$(path).each(function(i,e) {
a.push(...)
b.push(value)
c.push(getFile(value))
})
var entry = new DB...//(a,b,c)
addToDB(entry);
}
function getFile(...) {
request(fullUrl, function (err, resp, page) {
if (!err && resp.statusCode == 200) {
var $ = cheerio.load(page); // load the page
srcEp = $(this).attr("src");
return srcEp;
} // end error and status code
}); // end request
}
I've been reading about promises/callbacks and then() but I've yet to find anything which works.
First, you have to get your mind around the fact that any process that relies, at least in part, on an asynchronous sub-process, is itself inherently asynchronous.
At the lowest level of this question's code, request() is asynchronous, therefore its caller, getFile() is asynchronous, and its caller, getInfo() is also asynchronous.
Promises are an abstraction of the outcome of asynchronous processes and help enormously in coding the actions to be taken when such processes complete - successfully or under failure.
Typically, low-level asynchronous functions should return a promise to be acted on by their callers, which will, in turn, return a promise to their callers, and so on up the call stack. Inside each function, returned promise(s) may be acted on using promise methods, chiefly .then(), and may be aggregated using Promise.all() for example (syntax varies).
In this question, there is no evidence that request() currently returns a promise. You have three options :
discover that request() does, in fact, return a promise.
rewrite request() to return a promise.
write an adapter function (a "promisifier") that calls request(), and generates/returns the promise, which is later fulfilled or rejected depending on the outcome of request().
The first or second options would be ideal but the safe assumption for me (Roamer) is to assume that an adapter is required. Fortunately, I know enough from the question to be able to write one. Cheerio appears not to include jQuery's promise implementation, so a dedicated promise lib will be required.
Here is an adapter function, using syntax that will work with the Bluebird lib or native js promises:
//Promisifier for the low level function request()
function requestAsync(url) {
return new Promise(function(resolve, reject) {
request(url, function(err, resp, page) {
if (err) {
reject(err);
} else {
if (resp.statusCode !== 200) {
reject(new Error('request error: ' + resp.statusCode));
}
} else {
resolve(page);
}
});
});
}
Now getFile(...) and getInfo() can be written to make use of the promises returned from the lowest level's adapter.
//intermediate level function
function getFile(DOMelement) {
var fullUrl = ...;//something derived from DOMelement. Presumably either .val() or .text()
return requestAsync(fullUrl).then(function (page) {
var $ = cheerio.load(page);
srcEp = $(???).attr('src');//Not too sure what the selector should be. `$(this)` definitely won't work.
return srcEp;
});
}
//high level function
function getInfo(path) {
var a = [], b = [], c = [];//presumably
// Now map the $(path) to an array of promises by calling getFile() inside a .map() callback.
// By chaining .then() a/b/c are populated when the async data arrives.
var promises = $(path).map(function(i, e) {
return getFile(e).then(function(srcEp) {
a[i] = ...;
b[i] = e;
c[i] = srcEp;
});
});
//return an aggregated promise to getInfo's caller,
//in case it needs to take any action on settlement.
return Promise.all(promises).then(function() {
//What to do when all promises are fulfilled
var entry = new DB...//(a,b,c)
addToDB(entry);
}, function(error) {
//What to do if any of the promises fails
console.log(error);
//... you may want to do more.
});
}
I'm using jQuery to make various ajax POST requests. I need to keep track of the success or failure of each one of them, along with the overall progress of the complete batch, so that I can update the UI with a progress bar and info about how many requests have succeeded, and how many have failed, out of the total.
Before attempting to implement the feature in my app, I've been playing with some code in jsfiddle as a proof of concept, with no luck so far. This is what I've got:
// an alternative to console.log to see the log in the web page
var fnLog = function(message) {
$('#console').append($("<p>" + message + "</p>"));
};
// keeping track of how many ajax calls have been finished (successfully or not)
var count = 0;
// a dummy ajax call that succeeds by default
var fn = function(shouldFail) {
return $.get(shouldFail ? '/echo/fail/' : '/echo/json/')
.done(function() { fnLog("done") })
.fail(function() { fnLog("FAIL") });
};
// a set of different asynchronous ajax calls
var calls = [fn(),fn(),fn(),fn(true),fn(),fn()];
// an attempt to make a collective promise out of all the calls above
$.when.apply($, calls)
.done(function() { fnLog("all done") })
.fail(function() { fnLog("ALL FAIL") })
.always(function() { fnLog("always") })
.progress(function(arg) { fnLog("progress" + arg) })
.then(function() { fnLog("finished") });
It's all in this fiddle: http://jsfiddle.net/mmtbo7v6/1/
What I need is the ability to provide a callback that ought to be called after all promises are resolved (either successfully or not).
When all calls above are set to succeed (by removing the true argument to the fourth fn call in the array) it works fine. The output prints the following:
done
done
done
done
done
done
all done
always
finished
But when even a single call is set to fail (as it is by default in the jsfiddle), the output is the following:
done
FAIL
ALL FAIL
always
done
done
done
done
So none of the collective promise callbacks (the one generated by the $.when call) is called after all promises are resolved. The final .then is not called at all if a single ajax call fails.
Additionally, I would appreciate some insight on how to keep track of the progress of this batch of ajax calls, to update a progress bar in the UI.
Well... I'm going to be unfair. jQuery actually comes bundled with progression events but I myself hate them because I don't think they compose or aggregate well - so I'll show a simpler alternative approach for that progress bar that I believe is superior instead.
First thing's first:
The 'all promises resolved but some possibly rejected' issue is called a 'settle' typically. I've provided an answer to a similar question here with just giving the results and here providing an implementation that gives you access to all results even rejected ones.
function settle(promises){
var d = $.Deferred();
var counter = 0;
var results = Array(promises.length);
promises.forEach(function(p,i){
p.then(function(v){ // add as fulfilled
results[i] = {state:"fulfilled", promise : p, value: v};
}).catch(function(r){ // add as rejected
results[i] = {state:"rejected", promise : p, reason: r};
}).always(function(){ // when any promises resolved or failed
counter++; // notify the counter
if (counter === promises.length) {
d.resolve(results); // resolve the deferred.
}
});
});
return d.promise();
}
You'd use settle in place of $.when to get your desired results.
As for progression - I personally recommend passing a progression callback to the method itself. The pattern goes something like this:
function settle(promises, progress){
progress = progress || function(){}; // in case omitted
var d = $.Deferred();
var counter = 0;
var results = Array(promises.length);
promises.forEach(function(p,i){
p.then(function(v){ // add as fulfilled
results[i] = {state:"fulfilled", promise : p, value: v};
}).catch(function(r){ // add as rejected
results[i] = {state:"rejected", promise : p, reason: r};
}).always(function(){ // when any promises resolved or failed
counter++; // notify the counter
progress((promises.length - counter) / promises.length);
if (counter === promises.length) {
d.resolve(results); // resolve the deferred.
}
});
});
return d.promise();
}
Which would let you do something like:
settle([url1, url2, ... url100].map($.get), function(soFar){
$("#myProgressBar").css("width", (soFar * 100)+"%");
}).then(function(results){
console.log("All settled", results);
]);
It turns out there's a much better alternative to this problem, one which shadows the promises approach. Behold the combination of two patterns: Observables + Iterables = Reactive programming.
Reactive Programming is programming with asynchronous data streams, that is, treating asynchronous data streams as collections that can be traversed and transformed as traditional collection data types. This article is a great introduction.
I won't convert this answer post into a tutorial, so let's go straight to the solution, which is shown below. I'm gonna be using the RxJS library, but there are other libraries for Reactive Programming in JS (bacon.js seems to be really popular too).
function settle(promises) {
return Rx.Observable.from(promises).concatMap(function(promise, index) {
return Rx.Observable.fromPromise(promise).
map(function(response) {
return { count: index+1, total: promises.length, state: "fulfilled", promise: promise, value: response };
}).
catch(function(reason) {
return Rx.Observable.of({ count: index+1, total: promises.length, state: "rejected", promise: promise, reason: reason });
});
});
}
The function itself returns an observable, which is a stream of events. Namely, the events of each promise finished, either successfully or not. We can use that returned observable to listen to this stream (or to subscribe to it, if we're to adhere to RxJS terminology).
var results = settle(promises);
results.subscribeOnNext(function(results) {
// process each result as it arrives
// progress info can be extracted from results.count and results.total
});
results.subscribeOnCompleted(function() {
// completion callback
});
And that's it. Much cleaner code, a more functional-programming approach. No need to keep state, everything expressed in a more declarative way. Just what we want to be done, not how it should be done.
I have a function that does a series of asynchronous actions that in turn execute loops of other asynchronous actions. I'd like to know when everything is complete. It seemed like a great time to get my head wrapped around promises.
My code in the before-promise state boils down to something like this (hopefully in the simplification process I haven't rendered the example useless):
myClass.prototype.doMaintenance = function() {
var types = ['choreType1', 'choreType2'];
types.forEach(function(choreType) {
// find all chores of the type with score 0 (need to be done)
redisClient.zrangebyscore('chores:'+choreType, 0, 0, function(err, chores) {
if (!err) {
chores.foreach(function(chore) {
doChore(chore, function(err, result){
// chore complete!
});
})
}
});
});
}
I go through a loop, and for each item in the loop I make an asynchronous database call, and loop through the results returned, making another asynchronous call for each result. Using callbacks to pass notification that all chores are done seems like it would be ugly at best. Therefore my goal: construct a promise that will resolve when all the chores are done.
I'm facing two difficulties. One is simply getting the promise syntax right. I'll show you what I've tried below. First through, an issue that may render this insolvable: say that the first database query comes back with a single chore. I (somehow) put that as part of the overall "all chores done" promise. Now I go back to get a list of the next type of chore. What if in the meantime the first chore is completed? The all-chores-done promise will be satisfied, and will resolve, before the rest of the chores are added.
I'm using the Q library in a node.js environment. I use Redis but it could be any asynch data source.
myClass.prototype.doMaintenance = function() {
var types = ['choreType1', 'choreType2'];
var typePromises = [];
types.forEach(function(choreType) {
// find all chores of the type with score 0 (need to be done)
Q.npost(redisClient, 'zrangebyscore', ['chores:'+choreType, 0, 0]).done(chores) {
var chorePromises = [];
chores.foreach(function(chore) {
chorePromises.push(doChore(chore)); // doChore returns a promise
});
typePromises.push(Q.all(chorePromises));
});
});
return Q.all(typePromises); // at this point, typePromises is empty. Bummer!
}
What I've been trying to build (not quite there yet) is a promise that is a collection of typePromises, which in turn are collections of chorePromises.
I think what I need is a structure that says "I promise to get you the all-chores-done promise as soon as it's available." This is starting to make my head explode. Any guidance (including using a different pattern entirely) would be greatly appreciated.
You are constructing the list of typePromises asynchronically - and when you call Q.all(typePromises), it is still empty. Instead, you immediately need to return a promise for the database result which you can immediately collect into the list. If you don't know yet what the return value of those promises will be - no worries, use then to compose the tasks like getting Q.all(chorePromises) after a redis result has arrived.
I also would propose using map instead of pushing to an array in an each loop - this also helps to make sure that the promises are constructed immediately.
myClass.prototype.doMaintenance = function() {
var types = ['choreType1', 'choreType2'];
var typePromises = types.map(function(choreType) {
// find all chores of the type with score 0 (need to be done)
return Q.npost(redisClient, 'zrangebyscore', ['chores:'+choreType, 0, 0]).then(function(chores) {
var chorePromises = chores.map(doChore); // doChore returns a promise
return Q.all(chorePromises);
}); // then returns a promise
});
return Q.all(typePromises);
}