JavaScript result happening before callbacks complete - javascript

I'm totally new to JS having jumped in a few days ago to try make a chrome extension, so sorry if this is a simple problem, but I can't seem to figure it out.
My original function was to simply download an image and increment the stored count by 1 and add on the file size. However on a page of images it hit the write limits of chrome so I decided to count the values and write them at the end.
Initially the return value happened much later than when the function was executed (so it returned nothing), so I looked up how to fix it and got it working with a callback. However, although it waits for the callbacks, the code just continues past the callbacks and the part afterwards is executed before anything else, meaning the final count will always be 0.
// Loop through all urls
var approx_filesize = 0;
for(var i = 1; i < all_urls.length; i++){
var image_url = all_urls[i];
_download_image(image_url, folder_name, function(item){
approx_filesize += parseInt(item);
});
}
// This happens before any _download_image functions complete
alert('end' + approx_filesize);
// Write to storage
chrome.storage.sync.get({
download_count: 0,
download_size: 0
}, function(items) {
chrome.storage.sync.set({
download_count: parseInt(items.download_count) + all_images_data.length - 1,
download_size: parseInt(items.download_size) + approx_filesize
}, function() {
});
});
I just tried moving the loop into its own callback function and still had no luck, the alert runs before the first function completes.
function image_url_loop_callback(callback, folder_name, all_urls){
var approx_filesize = 0;
for(var i = 1; i < all_urls.length; i++){
var image_url = all_urls[i];
_download_image(image_url, folder_name, function(filesize){
approx_filesize += parseInt(filesize);
});
}
callback(approx_filesize);
}
image_url_loop_callback(function(approx_filesize){
alert(approx_filesize);
}, folder_name, all_urls);
How do I make it so that the loop completes before anything else is done?
Edit: Got it working with promise, here's the adjusted code:
new Promise( function(resolve, reject) {
var count = 1;
var num_items = all_urls.length;
var approx_filesize = 0;
for(var i = 1; i < num_items; i++){
var image_url = all_urls[i];
_download_image(image_url, folder_name, function(item){
approx_filesize += parseInt(item);
count ++;
if(count == num_items){
resolve([num_items, approx_filesize]);
}
});
}
}).then( function(stuff) {
var num_items = stuff[0];
var approx_filesize = stuff[1];
chrome.storage.sync.get({
download_count: 0,
download_size: 0
}, function(items) {
chrome.storage.sync.set({
download_count: parseInt(items.download_count) + num_items,
download_size: parseInt(items.download_size) + approx_filesize
}, function() {
});
});
});

Basically, you have to handle the asynchronous aspect of JavaScript.
To do so, you have to use a Promise.
This works this way:
new Promise( () => {
// My asynchronous code
}).then( () => {
// My code which need to wait for the promise resolution.
});
If you are working with only the latest versions of browsers, you can also have a look at async/await keywords which make asynchronous handling much easier than regular promises (but still are promises).
EDIT: As this answer required further explanation and proper code snippets, I edited it to answer a comment.
This example maybe easier to understand:
let myFoo = "Hello";
test = new Promise( (resolve) => {
console.log(myFoo);
myFoo = "World!";
setTimeout(() => {
resolve();
}, 4000);
}).then( () => {
console.log(myFoo);
});
This will print "Hello" immediately, and "World!" 4 seconds after.
This is how you work with promises. You can perfectly edit variables which are defined in a scope outside of the promise. Please don't use var, just stick to let and define a decent scope.

Due to javascript's async nature you have to use promises:
https://developers.google.com/web/fundamentals/getting-started/primers/promises

Related

Javascript Code will not click each element of an accordion

I am trying to write a script that will find every part of an accordion and click it, after this code runs I will scrape. But, I am having issues with each part being clicked, only a few do or only on the very last element is all the clicking be done.
The url is: https://www.vikingrivercruises.com/cruise-destinations/europe/danube-waltz/2022-budapest-passau/pricing.html
The code I am using is:
var elements_accordion = document.querySelectorAll('[data-bind="text: priceRange"]');
for(var i = 0; i < elements_accordion.length;i++)
{
async function output_print(i)
{
await elements_accordion[i].click();
await console.log("Doing number: "+i);
}
async function timeOutFn(i) { await setTimeout(output_print, 50000, i); }
timeOutFn(i);
}
Further, if I input into the console: elements_accordion[1].click() it does what I want but not when it goes into a for loop. I was thinking its an asynchronous issue, but only occasionally does it work 100%.
Any help would be great.
There is no point in using async/await logic on console.log,
click, and setTimeout function because these functions don't
return Promise.
Here is the working example with proper sleepFn function which returns a promise.
const accordion_elements = document.querySelectorAll('tr.hidden-tn.hidden-xs');
const sleepFn = ms => new Promise((res) => setTimeout(() => res(), ms));
accordion_elements.forEach(async function(item) {
await sleepFn(1000);
item.click()
}
);
Try this
var moreInfoElements = document.getElementsByClassName("more-info");
var timeDelay = 4000;
x=0;
while(x<moreInfoElements.length) {
setTimeout(clickMe,timeDelay*x,moreInfoElements[x],x);
x++;
}
function clickMe(elemToClick,num) {
console.log("clicking "+(num+1)+" out of "+moreInfoElements.length);
elemToClick.click();
}

Deferred Promise - Resend same request if callback doesn't meet criteria

I am trying to achieve the following functionality:
execute call back
resolve promise
check output
if not correct execute again
I have 'mimicked' the scenario with a timer, this reruns a script that makes a call to backend database for some information:
_runCheckScript: function(bStart, bPreScript){
var oController = this;
var scriptTimerdeferred = $.Deferred();
var promise = scriptTimerdeferred.promise();
if(typeof(bStart) === "undefined"){
bStart = true;
}
if(typeof(bPreScript) === "undefined"){
bPreScript = true;
}
// if the HANA DB is not stopped or started, i.e. it is still starting up or shutting down
// check the status again every x number of seconds as per the function
var msTime = 10000;
if(!bPreScript){
this._pushTextIntoConsoleModel("output", {"text":"The instance will be 'pinged' every " + msTime/1000 + " seconds for 2 minutes to monitor for status changes. After this, the script will be terminated."});
}
if(bPreScript){
var timesRun = 0;
var commandTimer = setInterval( function () {
timesRun += 1;
if(timesRun === 12){
scriptTimerdeferred.reject();
clearInterval(commandTimer);
}
// send the deferred to the next function so it can be resolved when finished
oController._checkScript(scriptTimerdeferred, bStart, bPreScript);
}, msTime);
}
return $.Deferred(function() {
var dbcheckDeffered = this;
promise.done(function () {
dbcheckDeffered.resolve();
console.log('Check finished');
oController._pushTextIntoConsoleModel("output", {"text":"Check finished."});
});
});
The script it calls, has it's own promise as it calls another function:
_checkScript: function(scriptTimerdeferred, bStart, bPreScript){
var oProperties = this.getView().getModel("configModel");
var oParams = oProperties.getProperty("/oConfig/oParams");
var deferred = $.Deferred();
var promise = deferred.promise();
var sCompareStatus1 = "inProg";
var sCompareStatus2 = this._returnHanaCompareStatus(bStart, bPreScript);
var sCompareStatus3 = this._returnHanaCompareStatus3(bStart, bPreScript);
var params = {//some params};
// Send the command
this._sendAWSCommand(params, deferred);
// When command is sent
promise.done(function (oController) {
console.log('back to db check script');
var oCommandOutputModel = oController.getView().getModel("commandOutput");
var sStatus = oCommandOutputModel.Status;
// check that it's not in the wrong status for a start/stop
// or if it's a pre script check -> pre script checks always resolve first time
if(sStatus !== sCompareStatus1 && sStatus !== sCompareStatus2 && sStatus !==sCompareStatus3|| bPreScript){
scriptTimerdeferred.resolve();
}
});
},
This works, however what it does is:
set a timer to call the first script every x seconds (as the data is currently changing - a server is coming online)
the script runs and calls another function to get some data from the DB
when the call for data is resolved (complete) it comes back to 'promise.done' on the checkScript and only resolves the timer promise if it meets certain criteria
all the while, the initial timer is resending the call as eventually the DB will come online and the status will change
I am wondering if there is a better way to do this as currently I could have, for example, 3 calls to the DB that go unresolved then all resolve at the same time. I would prefer to run a command, wait for it to resolve, check the output, if it is not right then run command again.
Thanks!
I think what you want to do can be achieved carefully reading what explained in these links:
Promise Retry Design Patterns
In javascript, a function which returns promise and retries the inner async process best practice
See this jsfiddle
var max = 5;
var p = Promise.reject();
for(var i=0; i<max; i++) {
p = p.catch(attempt).then(test);
}
p = p.then(processResult).catch(errorHandler);
function attempt() {
var rand = Math.random();
if(rand < 0.8) {
throw rand;
} else {
return rand;
}
}
function test(val) {
if(val < 0.9) {
throw val;
} else {
return val;
}
}
function processResult(res) {
console.log(res);
}
function errorHandler(err) {
console.error(err);
}
It retries a promise infinite times since the condition is not satisfied. Your condition is the point you said "check the output". If your check fails, retry the promise. # Be careful to hold a limit case, promises waste memory. If your api/service/server/callreceiver is off, and you don't set a threshold, you could create an infinite chain of promises NO STOP

Best way to write loops with promises (ctx.sync) in JavaScript API for Office

There are many threads that discuss about guaranteeing execution order of promises in loops. I would like to know what is the best practice in JavaScript API for Office Add-ins. Most of the time, the promise in question is ctx.sync().
Here is a snippet to print the address of a list of Excel ranges one by one. The test shows that it respects well the order of Excel ranges. But the question is whether and how to guarantee the execution order?
function loadAll () {
var ranges = ["A:A", "B:B", "C:C", "D:D", "E:E"];
var sheet = "Sheet1";
for (var i = 0; i < ranges.length; i++) {
loadRange(ranges[i], sheet);
}
}
function loadRange (range, sheet) {
Excel.run(function (ctx) {
var r = ctx.workbook.worksheets.getItem(sheet).getRange(range);
r.load('address');
return ctx.sync().then(function() {
console.log(r.address);
});
});
}
Could anyone help?
Because Excel.run returns a Promise, you can chain it with a .then and guarantee order. I.e.,
Excel.run(function(ctx) { ... return ctx.sync(); ... })
.then(function() {
return Excel.run(function(ctx) { ... return ctx.sync(); ... })
})
.then(function() {
return Excel.run(function(ctx) { ... return ctx.sync(); ... })
});
That being said... this would be pretty dang inefficient. A much better approach would be to load all the objects you need in one batch, creating only one network roundtrip (especially important with Excel Online... but noticeable even on the Desktop):
function loadAll () {
Excel.run(function(ctx) {
var ranges = ["A:A", "B:B", "C:C", "D:D", "E:E"];
var sheet = "Sheet1";
var loadedRanges = [];
for (var i = 0; i < ranges.length; i++) {
var r = ctx.workbook.worksheets.getItem(sheet).getRange(ranges[i]);
r.load('address');
loadedRange.push(r);
}
return ctx.sync()
.then(function() {
for (var i = 0; i < loadedRanges.length; i++) {
console.log(loadedRanges[i].address);
}
});
});
}
UPDATE
If, as per comment, you do end up needing to do separate tasks that depend on each other and that each require a roundtrip, and hence do need to be sequenced via chaining Excel.run, I would recommend something as follows:
function loadAll () {
var ranges = ["A:A", "B:B", "C:C", "D:D", "E:E"];
var sheet = "Sheet1";
// Create a starter promise object
var promise = new OfficeExtension.Promise(function(resolve, reject) { resolve (null); });
for (var i = 0; i < ranges.length; i++) {
// Create a closure over i, since it's used inside a function that won't be immediately executed.
(function(i) {
// Chain the promise by appending to it:
promise = promise.then(function() {
return loadRange(ranges[i], sheet);
})
})(i);
}
}
function loadRange (range, sheet) {
return Excel.run(function (ctx) {
var r = ctx.workbook.worksheets.getItem(sheet).getRange(range);
r.load('address');
return ctx.sync().then(function() {
console.log(r.address);
});
});
}
~ Michael Zlatkovsky, developer on Office Extensibility team, MSFT
The update of #Michael Zlatkovsky solves the problem of appending promises perfectly.
The complement added by #SoftTimur allows to wait for all promises to be done before doing a .then(), which is also very convenient !
My only remark about these posts would be if ANY promises throws an error, the other appended promises stop being treated.
In my case, the scenario is a little different. Just to clarify:
Excel.run(function(context){
return runWorkbook(context, context.workbook)
.then(function(){ var cool = "all promises worked !" }
.catch(function(error)) { var bad = "do not want to be here :(" });
}
function runWorkbook(context, workbook){
const sheets = workbook.worksheets;
sheets.load("$none");
return context.sync().then(function(){
let promise = new window.OfficeExtension.Promise(function(resolve, reject) { resolve(null); });
sheets.items.forEach(function(ws) {
promise = promise.then(function() {
return makeWorkOnWorksheet(ws)
.then(context.sync())
.catch(function(error)){
// DO NOTHING BUT CAN NOT THROW ERROR OTHERWISE IT BREAKS THE NEXT APPENDED PROMISES
});
}
return promise;
}
}
This solution works.. (catch the error as in comment and doing nothing with it)
I don't like this solution but this is the only way I found to allow all appended promises to be done.
If somebody has a better idea, it's welcomed ;)
Cheers,

Javascript: why can't I wait for async task to finish using while loop?

The goal of the following code is to fetch webpages from three sites specified on command line and to display them in the order specified on command line. My first solution (shown below) was to use while loop to wait for three async tasks to finish but the while loop keeps looping forever. I learned that the correct solution was to detect if variable "count" reached 3 inside each async task (also shown below but commented out), not outside async task. But I'm wondering why my initial solution does not work. Can someone point me to the right Javascript specification of something that explains the flaw of my initial solution? Thanks!
var http = require('http');
var bl = require('bl');
var url1 = process.argv[2];
var url2 = process.argv[3];
var url3 = process.argv[4];
var content1;
var content2;
var content3;
var count = 0;
http.get(url1, function (res) {
res.pipe(bl(function (err, data) {
content1 = data.toString();
count++;
// if (count == 3) printAll();
}));
});
http.get(url2, function (res) {
res.pipe(bl(function (err, data) {
content2 = data.toString();
count++;
// if (count == 3) printAll();
}));
});
http.get(url3, function (res) {
res.pipe(bl(function (err, data) {
content3 = data.toString();
count++;
// if (count == 3) printAll();
}));
});
function printAll() {
console.log(content1);
console.log(content2);
console.log(content3);
}
// this while loop loops forever
while (count < 3) {
console.log('waiting..');
};
printAll();
setTimeout is not the same as sleep. It's a way to set a callback to happen in x milliseconds. You're passing null as the callback, and infinitely looping in the while loop, never relinquishing the thread to the http calls.
Edit: I would recommend looking into the async library for doing things like "wait for these three callbacks to finish".
You can fix this by using recursive function that call itself using setTimeout:
(function loop() {
if (count < 3) {
setTimeout(loop, 1000);
console.log('waiting..');
} else {
printAll();
}
})();

phantomjs not rendering webpage to png [duplicate]

This question already has an answer here:
PNG is not being rendered using PhantomJS with multiple attempts in a loop
(1 answer)
Closed 6 years ago.
well my code is something like this just a few lines
var a = "http://lnmtl.com/chapter/renegade-immortal-chapter-";
var b = 558;
var d = "rennegrade_ch";
var f = ".png";
var page = require('webpage').create();
var i = 0;
for (i = b; i < 560; i++) {
var c = a + i;
console.log(c);
page.open(c, function () {
var e = d + i + f;
console.log(e);
page.render(e);
});
}
phantom.exit();
the webpage can be rendered individually but once i put it inside for loop all it does is print the first console output properly but the second one it skips i guess its not entering the page.open function then for loop value increases then same thing happens again I have no idea why its not entering render function i tried to put var page = require('webpage').create();
inside for loop too but still no change
UPDATE: On another question stackoverflow.com/questions/31621577/png-is-not-being-rendered-using-phantomjs-with-multiple-attempts-in-a-loop?rq=1
it was pointed that this method wont work because of async nature of function but the example code provided in it isnt helpful enough can anyone example and i also tried set timeout as suggested in it still same thing happens so any other idea ?
Your phantom.exit() call kills the PhantomJS browser before you do any rendering. You have to wait for the rendering to end before you can exit(). You need to have some mechanism to say when the rendering is done. I'd suggest wrapping each of your renders in a Promise. Then using a Promise.all() to wait for all the render promises to resolve. After they resolve, exit PhantomJS.
Right now, you have what is below, which does not respect the asynchronous nature of page.open():
for (...) {
// I probably wont finish because phantom dies almost immediately
page.open(c, function () {
// I won't finish running since phantom dies
page.render(e);
});
}
// I'm going to kill the phantom almost immediately
phantom.exit();
You want something like the code below, which will wait for all the renders to finish. This will put renders of each of the sites we provide in a subdirectory "renders".
Note: You will need to install the es6-promise shim for this to work since PhantomJS does not yet support Promises. Thanks for the comment about that Artjon B
/*jslint node:true*/
/*globals phantom, sayHello*/
"use strict";
var Promise = require("es6-Promise").Promise;
// Array of URLs that we want to render
var urlArr = [
{
name: "google",
url: "http://www.google.com"
},
{
name: "yahoo",
url: "http://www.yahoo.com"
},
{
name: "bing",
url: "http://www.bing.com"
}
];
// Map URLs to promises
var proms = urlArr.map(function (url) {
// Return a promise for each URL
return new Promise(function (resolve, reject) {
// Make a page
var page = require("webpage").create();
// Open the URL
console.log("opening: " + url.name);
page.open(url.url, function () {
// Render the page
page.render("render/" + url.name + ".png");
console.log("done rendering: " + url.name);
// Say that we are done with rendering
resolve();
});
});
});
// Wait for all rendering to finish
Promise.all(proms).then(function () {
console.log("closing phantom");
// Exit phantom
phantom.exit();
});
For async request inside loop you should use asynchronous library so you can debug your code and don't get memory leak issue
async-js will be good in your case
npm install async
var async = require('async');
var a = "http://lnmtl.com/chapter/renegade-immortal-chapter-";
var b = 558;
var d = "rennegrade_ch";
var f = ".png";
var page = require('webpage').create();
var i = 0;
async.whilst(
function() {
return i < 560;
},
function(callback) {
i++;
var c = a + i;
console.log(c);
page.open(c, function() {
var e = d + i + f;
console.log(e);
page.render(e);
callback(null, i);
});
},
function(err, n) {
if(err) console.log(err);
phantom.exit();
});

Categories

Resources