Promise resolve misses one value for asynchronous request loop - javascript

Thanks to the help from here I could build a loop for posts and resolve the promises in order to handle asynchronous requests. But for some reason the loop to get the resolved promises and according values always misses one step. I tried to apply JotaBe's answer and it worked out, except there is one value missing. Everything else is fine.
The simplified function I call is:
var logs = function (outString, saveCSV) {
var promises = [];
var count = 0;
for (i = 1; i <= maxDevice; i++) {
promises.push($.post(Type1));
promises.push($.post(Type2));
promises.push($.post(Type3));
}
promises.push($.post(Type4));
var promiseResolve = $.when.apply($, promises);
promiseResolve.then(function () {
console.log(promises[1].promise().state());
console.log(promises[2].promise().state());
for (i = 0; i < promises.length; i++) {
promises[i].then(function (data, textStatus) {
var src = this.url;
var arg = arguments;
console.log(i + ": " + textStatus);
if (posttype2 or 3){String1 += data
} else if (posttype4) > 0)
{
String2 += data
} else
{
string3 += data
}
});
}
outString += String3+ "\n" + String2+"\n" + string1;
saveCSV(outString, filename);
});
};
The console.log(i + ": " + textStatus);shows
0: success
2: success
3: success
4: success
5: success
6: success
7: success
8: success
9: success
So i = 1is never resolved, even though console.log(promises[1].promise().state()); states promises[1] IS resolved.
If I set a breakpoint before the promiseResolve the missing promise is handled, though, while delaying the code with a timeout doesn't seem to help.
I also tried to use .done, instead of .then with the same result. The data missing is the largest data package of the loop. It can't be the syntax, since the other promises fetched with the same get in the loop resolve just fine.
So what am I doing wrong, as I can't understand why this one is missing. As far as I understood $when.applyis used to make sure the promises are resolved (which the console log states are).
How to handle this to get ALL values into the outstring?
Edit:
I tried some more console.log lines to check for values, or if something isn't resolved. So a console.log(i); right before the promises[i].then(function (data, textStatus)´ shows i = 1 is called. The row is complete 0, 1, 2... but theconsole.log(i + ": " + textStatus);after the function misses the 1, so it shows ´promises[1].then(function (data, textStatus) {...} is not called.
Logging
console.log(promises[1].promise().state());
console.log(promises[1].responseText);
right before the for.. loop shows the promise state is "resolved" and the responseText shows the string I want to attach to the outstrings. I also tried the proposed solution of Jaromanda X, but it did not help (thanks for the time), neither did using different combinations of using .doneinstead of .thenin either resolve function.
Putting a breakpoint before the promiseResolve.then seems to help, even if I click "run" as fast as I can. Have to try shortening that time, to be sure.
Adding another set of things I tried:
Splitting the posts/gets and the resolve into two functions and using the resolve as callback brought no success, neither did changing i to some unused variable, nor using for promise in promises to initiate the loop. I start running out of ideas to try, even more as promiseResolve returns resolved right at the promiseResolve.then function, so the request in promise[1] should be finished.
What seems to work, though I do not understand why and doesn't feel like the right way to solve the problem is encapsulating everything inside the promiseResolve.then function into a window.setTimeout(function(){...},1, so it looks more like
window.setTimeout(function(){
for (i=0 ; i < promises.length; i++) {
...
};
outString += spotString + "\n" + ioString + "\n" + logString;
saveCSV(outString, filename);
}, 1);
So, this one ms delay helps, but it doesn't feel like a clean solution. Can anyone explain why, or what I am missing? There must be a better way.

you have asynchronous code within the for loop that is using i
try this code
promiseResolve.then(function () {
console.log(promises[1].promise().state());
console.log(promises[2].promise().state());
for (i = 0; i < promises.length; i++) {
(function(i) { // ***** add this
promises[i].then(function (data, textStatus) {
var src = this.url;
var arg = arguments;
console.log(i + ": " + textStatus);
if (posttype2 or 3) {
String1 += data
} else if (posttype4) > 0) {
String2 += data
} else {
string3 += data
}
});
}(i)); // ***** add this
}
outString += String3 + "\n" + String2 + "\n" + string1;
saveCSV(outString, filename);
});
You'll have to make the changes from the invalid javascript to your actual code as above ... the additional lines are marked
what that code does is create a closure where i wont be changed before it is logged due to the asynchronous nature of the function it is logged in

Related

Only Push success callback data to array, exclude error 404

I have issues with handling error 404. What I want to do is, the data I get from making a request to the PokeApi, I want to push it into an array. If all request are successful to the API and pushed into the array, everything works fine, then I can get out my data I want, move it to my then() and append it to a div. However, once a request are getting a 404 error not found, nothing gets appended to my div. How do I handle the 404 and continue to loop and be able to append my data?
I have tried, using if statements and fail(), done() but just doesn't work as I want it to.
function get_sprites(poke_name){
var arr = [];
for(var i = 0; i<poke_name.length; i++){
arr.push($.getJSON("https://pokeapi.co/api/v2/pokemon/" +
poke_name[i]));
}
$.when.apply($, arr).then(function(){
var storeObjPoke = [];
for(var i = 0; i < arguments.length; i++){
var name = arguments[i][0].name
var upperCase_name = name.charAt(0).toUpperCase() +
name.slice(1);
var objPoke = {
id : arguments[i][0].id,
name : upperCase_name,
imgUrl : arguments[i][0].sprites.front_default
}
$("<div class='pokemon' id='"+ arguments[i][0].id +"'>" +
upperCase_name + "<br><img src='" +arguments[i]
[0].sprites.front_default+"'alt=''/><br>" + arguments[i][0].id +
"</div>" ).appendTo("#display_pokemon");
}
}
}
I expect it to be able to display all my objects in my div, but when I get a 404 error, nothing gets appended to div.
So it looks like .when is similar to promise.all with the fact that it's an all or nothing kinda thing. All promises have to be resolved (successful) for the .then function to be fired.
So what is happening for you is once the promise with the 404 is rejected, jQuery calls the .fail() function immediately (which you don't have so its swallowing the error) and the rest of the promises may be unfulfilled and .then() is not called.
From jQuery.when()
In the case where multiple Deferred objects are passed to jQuery.when(), the method returns the Promise from a new "master" Deferred object that tracks the aggregate state of all the Deferreds it has been passed. The method will resolve its master Deferred as soon as all the Deferreds resolve, or reject the master Deferred as soon as one of the Deferreds is rejected.
and
In the multiple-Deferreds case where one of the Deferreds is rejected, jQuery.when() immediately fires the failCallbacks for its master Deferred. Note that some of the Deferreds may still be unresolved at that point.
so what you are trying to do doesn't seem possible without some extra fluff. You can check out this SO answer to get a more general understanding of some of the fluff you might need Wait until all ES6 promises complete, even rejected promises including a future proposal promise. allSettled() which would solve your problem, but its only compatible with a few browsers currently.
2 other thoughts
Is it important that you wait for all requests to come back before you start displaying them in the UI? If not then just call the requests in a loop.
For example,
get_sprites(["squirtle", "charmander", "not_a_pokemon", "bulbasaur"])
function get_sprites(poke_name) {
for (let i = 0; i < poke_name.length; i++) {
$.getJSON("https://pokeapi.co/api/v2/pokemon/" + poke_name[i], addPokemon)
.fail(function(err) {
console.log(`error getting pokemon: ${poke_name[i]} - ${err.status}`);
});
}
function addPokemon(pokemon) {
var storeObjPoke = [];
var name = pokemon.name
var upperCase_name = name.charAt(0).toUpperCase() + name.slice(1);
var objPoke = {
id: pokemon.id,
name: upperCase_name,
imgUrl: pokemon.sprites.front_default
}
$("<div class='pokemon' id='" + pokemon.id + "'>" +
upperCase_name + "<br><img src='" + pokemon.sprites.front_default + "'alt=''/><br>" + pokemon.id +
"</div>").appendTo("#display_pokemon");
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="display_pokemon"></div>
If it is important to not show anything until all requests are finished, just keep a counter on the requests finished using .always() on the request and check after each request if the counter equals poke_name.length. If so, then you know a request for each pokemon has finished (success or fail) and then display all the results.
example of 2
get_sprites(["squirtle", "charmander", "not_a_pokemon", "bulbasaur", "onix", "mewtwo", "pikachu"])
function get_sprites(poke_names) {
let poke_names_length = poke_names.length;
let finished_requests = 0;
let poke_array = [];
for (let poke_name of poke_names) {
$.getJSON("https://pokeapi.co/api/v2/pokemon/" + poke_name)
.always(function() {
finished_requests++
})
.done(addPokemon)
.fail(function(err) {
console.log(`error getting pokemon: ${poke_name} - ${err.status}`);
})
}
function addPokemon(pokemon) {
let upperCase_name = pokemon.name.charAt(0).toUpperCase() + pokemon.name.slice(1);
let objPoke = {
id: pokemon.id,
name: upperCase_name,
imgUrl: pokemon.sprites.front_default
}
poke_array.push(objPoke);
if (poke_names_length === finished_requests) {
displayPokemon();
}
}
function displayPokemon() {
let ordered_pokes = poke_array.sort(function(a, b) {
return a.id - b.id;
});
for (let poke of ordered_pokes) {
$("<div class='pokemon' id='" + poke.id + "'>" +
poke.name + "<br><img src='" + poke.imgUrl + "'alt=''/><br>" + poke.id + "</div>").appendTo("#display_pokemon");
}
}
}
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.3.1/jquery.min.js"></script>
<div id="display_pokemon"></div>

Array returning Undefined because of asynchrony

I am accessing the API Trello, but I came across the following problem:
Trello access the information, getting the id of each existing row, the code is as follows:
var x;
var numberCardsByList = [];
trello.get("/1/boards/[idBoard]/lists/all", function(err, data) {
if (err) throw err;
console.log("Number of list: " + data.length);
for(var i=0; i<data.length; i++){
x = data[i];
findNumberCards(x);
}
});
As you can see, after getting the size, I walk all these queues with is, within the loop, attach each row in a variable x and call a function that aims to get the number of cards that queue. The code for the number of cards is as follows:
function findNumberCards(x){
trello.get("/1/lists/"+x.id+"/cards", function(err, dados){
if(err) throw err;
console.log("Name List: " + x.name + " have " + dados.length + " cards");
numberCardsByList[x.name] = dados.length;
});
}
Until then all right, but when I try to access the vector numberCardsByList after the end of the search in Trello, it returns undefined:
var x;
var numberCardsByList = [];
trello.get("/1/boards/[idBoard]/lists/all", function(err, data) {
if (err) throw err;
console.log("Quantidade de Filas: " + data.length);
for(var i=0; i<data.length; i++){
x = data[i];
findNumberCards(x);
}
});
console.log(numberCardsByList);
I am aware that it is because of asynchrony, however, can not solve.
The problem you're facing has been solved many times before. If you want to know more, search for the keyword "Promise". If you're familiar with jQuery, try and look up: $.whenAll, $.ajax().done, $.ajax().always, etc.
If you want to come up with a light weight solution yourself, here's a pointer:
By the time you get to your console.log(numberCardsByList), your requests triggered by findNumberCards haven't yet completed, making the Array empty. You'll need to make sure you know when all findNumberCards requests have completed and then log them. Alternatively, you could log them every time one of them completes.
There are roughly two approaches:
Keep track of your open requests and call a function when a request is handled.
Observe your numberCardsByList object and call a function whenever items are added (you won't know if they were added async or synchronously)
I'd suggest going with the first approach. Check out this example code and the comments:
var numberCardsByList = {};
// This array will store the url for every open request
var openRequests = [];
var removeRequest = function(url) {
var index = openRequests.indexOf(url);
if (index === -1) return;
// Remove url from array
openRequests = openRequests
.slice(0, index)
.concat(openRequests
.slice(index + 1));
};
// This will be called whenever one request completes
var onComplete = function(url) {
removeRequest(url);
// When all have completed, we can call our callback
if (openRequests.length === 0) {
onAllComplete();
}
});
// This will be called when there are no open requests left
var onAllComplete = function(data) {
console.log(numberCardsByList);
}
trello.get("/1/boards/[idBoard]/lists/all", function(err, data) {
if (err) throw err;
console.log("Number of list: " + data.length);
for (var i = 0; i < data.length; i++) {
x = data[i];
findNumberCards(x);
}
});
function findNumberCards(x) {
var url = "/1/lists/" + x.id + "/cards";
// Before we make the request, we register it:
openRequests.push(url);
trello.get(url, function(err, dados) {
numberCardsByList[x.name] = dados.length;
// When it is completed, we call onComplete
onComplete(url);
});
};
Note that this onAllComplete isn't 100% safe: it might be called multiple times if a request finishes before the next one is started.
Concluding:
If you can, I'd use a library to handle promises. If you want to try and build something yourself, you could try and keep track of the requests and execute a callback when they've all completed.
Keep in mind my above code most likely wont work for you as i dont know whats going on in your code so this is an example / explanation how to deal with your problem.
Since you are unfamiliar with async operation i will assume you dont have a prior knowledge of promises and therefore give you a less optimal solution - however promises are alot better and you should defintely learn them.
You need to execute sequence procedures inside the result of the async code.
First you'll create a function for the second operation for example:
function numberCardsByList (param1,param2){.....}
You will then change fineNumberCards to also accept a callback:
function findNumberCards(x, callback){
trello.get("/1/lists/"+x.id+"/cards", function(err, dados){
if(err) throw err;
console.log("Name List: " + x.name + " have " + dados.length + " cards");
numberCardsByList[x.name] = dados.length;
});
// pass in any params you need.
callback();
}
And then you will pass the newly created function numberCardsByList to findNumberCards or wherever you want it.
trello.get("/1/boards/[idBoard]/lists/all", function(err, data) {
if (err) throw err;
console.log("Number of list: " + data.length);
for(var i=0; i<data.length; i++){
x = data[i];
// and here we are calling findNumberCards and passing in the callback..
findNumberCards(x, numberCardsByList);
}
});
That is generally how you will deal with async operation, you will pass a callback for the next operation to be executed.
update
here is an example of how this is done with another scenario just to demonstrate the point farther.
we start by getting user
service.getUser(userName, function(err,user){
if(user) {
// we get user picture passing getPictureSize as callback
getUserPicture(user.picture, getPictureSize)
}
})
we get the pictureURL
function getUserPicture(picName, cb){
service.getPictureURL(picName, function(err, pictureURL){
if(pictureURL) {
// we then call the callback - the next async operation we want.
cb(pictureURL);
}
});
}
we get picture size - which is the last operation
function getPictureSize(pictureURL){
service.getPictureSize(pictureURL, function(err, pictureSize){
$('.picName').attr('src', picName);
$('.picName').width(pictureSize.width);
$('.picName').height(pictureSize.height);
});
}
I hope that clarify things a little.

Check if a promise finished in Javascript?

I am using PDF.js to extract text content from a PDF which I will use next for some more processing, For this,
var complete=0;
var full_text="";
var PDF_render = PDFJS.getDocument("x.pdf").then(function(pdf) {
var page_text = {};
for (i = 1; i <= pdf.numPages; i++){
pdf.getPage(i).then( function(page){
var n = page.pageNumber;
page.getTextContent().then( function(textContent){
var page_text_part = "";
textContent.items.forEach(function (textItem){
page_text_part += textItem.str;
page_text_part += " ";
});
page_text[n] = page_text_part + "\n\n";
++complete;
if (complete == pdf.numPages){
for( var j = 1; j <= pdf.numPages; j++)
full_text += page_text[j];
}
});
});
}
});
The issue is that PDF.js returns promises and they are executed asynchronously, however I need to perform some post processing on the returned text. For this I need to wait for the promises to fully execute and only then move on. How does one achieve this? Please help.
Unfortunately this question asks one thing in title (how to check if Promise finished), and another in the body (how to continue once Promise is finished). Google points to this when searching for how to check if Promise is finished, so I'm answering the question in the title.
To check if a promise if is finished, you can use Promise.race - it will not wait for pending promise if given non-promise value, e.g.
const statusOrValue = await Promise.race([myPromise, 'pending']);
if (statusOrValue === 'pending') {
// Not yet finished
}
Note that this method gives the result asynchronously - await is necessary to check if promise is finished. I don't know if there is a reliable way to check whether promise is completed synchronously.
We can use a much more direct approach, no need for counters etc.
Two things - promises chain, and waiting for multiple promises is done with Promise.all:
var pdf = PDFJS.getDocument("x.pdf");
pdf.then(function(pdf) { // wait for the PDF to load
var pages = [];
for (i = 1; i <= pdf.numPages; i++){ // for each pages
pages.push(pdf.getPage(i).then(function(page){ // push the promise
return page.getTextContent();
}).then(function(textContent){
var page_text_part = "";
textContent.items.forEach(function (textItem){
page_text_part += textItem.str + " ";
});
return page_text_part + "\n\n"; // you can use return values
}));
}
return Promise.all(pages); // wait for all of them to be done
}).then(function(results){
// you can access all the results here
// containing all pages
});
You can use Promise.all to wait for multiple promises to have resolved.
In your case, the code should look like
PDFJS.getDocument("x.pdf").then(function(pdf) {
var pages = [];
for (var i = 1; i <= pdf.numPages; i++) {
pages.push(pdf.getPage(i).then(function(page) {
return page.getTextContent();
}).then(function(textContent) {
return textContent.items.map(function(textItem){
return textItem.str;
}).join(" ") + " \n\n";
});
return Promise.all(promises);
}).then(function(page_texts) {
var full_text = page_texts.join("");
// now do something with the result
});

Async Template loading

I'm trying to asynchronously load 25 html templates
Here's my code:
var loopingLoadTemplate = function(index){
var name = names[index];
$.get('templates/' + name + '.html', function (data) {
that.templates[name] = data;
tplCount++;
console.log(tplCount + " " + names.length);
if (tplCount === names.length){
callback();
}
});
};
for (i = 0; i < names.length; i++){
loopingLoadTemplate(i);
}
tplCount is a counter that I keep so I know when it's safe to fire the callback
the problem is that, there are 25 templates to load, and when I checked under network tab in Chrome console, I see all 25 templates getting loaded properly, but console.log tells me the tplCount stops at 21, which I have no idea why. Is it because the for loop is firing so fast that some callbacks of the $ functions did not fire?
How do I safely asynchronously load all these templates?
So I also tried an synchronously fallback using recursive calls, but it mysteriously stops after loading some templates and gives no warning sign
var loadTemplate = function (index) {
var name = names[index];
console.log("loading " + names[index]);
$.get('templates/' + name + '.html', function (data) {
that.templates[name] = data;
index++;
if (index < names.length) {
loadTemplate(index);
console.log("trying another load");
} else {
callback();
console.log("trying callback");
}
});
};
loadTemplate(0);
OK, just figured out why its failing
so, indeed all the templates are loaded properly, but, if a html template has no content, the data in the callback function becomes undefined, instead of being empty as what I had expected
when the data is undefined, it fails the line:
that.templates[name] = data;
without any warnings or errors and the rest of the code in the callback are not excuted
since all the templates do get loaded, checking the network tab would give all status success result

Callbacks on Multiple AJAX request termination

SOLVED - Due to a Simple Mistake in the HTML that messed up the associated Javascript
I'm trying to issue a callback using ".ajaxStop" after a series of many AJAX requests have been issued. However, my code is not invoking the callback after the requests have finished.
Here is my code:
$.getJSON('https://graph.facebook.com/me/mutualfriends/' +
friendArray[i] + "?access_token=" + accessToken,
(function(index) {
for (var i = 0; i < friendArray.length; i++){
return function(dataJSON2) {
resultArray = dataJSON2['data'];
resultJSON += '"' + friendArray[index] + '"' + ":" + resultArray.length;
if (index != friendArray.length - 1){
resultJSON += ",";
}else {
resultJSON += "}";
}
}
}) (i));
}
$('#messageContainer').ajaxStop( function() {
$(this).html(resultJSON);
});
Each of the callbacks for my AJAX requests are correctly put in a closured callback but when I wait for the ajaxStop method to trigger, nothing happens.
You have a scope and timing issue. 'Scope' because the variable resultJSON only exists within your $.getJSON callback method. 'Timing' because even if you managed to share the resultJSON variable, due to the asynchronous nature of the $.getJSON method, the resultJSON variable wouldn't populate in time to be used inside the $.ajaxStop method.

Categories

Resources