Apps Script - Update the user on code execution status - javascript

Some of the scripts that I run take a long time and users might get concerned that a script stopped working if they can't see the status/step. I have added a spinner to the Sidebar to at least indicate that the script started running, but I would like to do more than that.
Ideally, I would be able to directly update the Sidebar contents from the GAS, but I gather than is not possible because of sandboxing. I have seen other questions and answers that discuss using success handlers in a daisy chain like this:
function uploadActivities(){
google.script.run.withSuccessHandler(onSuccess).activities_upload();
}
function onSuccess(lastStatus){
$('#codestatus').text(lastStatus);
google.script.run.step_two();
}
It is a hack and it would require me to split the code into smaller steps and pass values to the UI, which don't belong in the UI, and back to the code. I really don't like that approach and maintenance could be a bear.
I have tried creating a var in GAS and updating that value as the code progresses. However, I can't find a way to get the UI to periodically check until the code execution is complete AND to successfully update the UI after each step.
Here is the code I have created:
function uploadActivities(){
google.script.run.activities_upload();
getStatus();
}
function getStatus(){
var isActive = true;
while(isActive){
var lastStatus = google.script.run.getStatus();
$('#codestatus').text(lastStatus);
if(lastStatus === 'Complete'){ isActive = false; }
}
}
In GAS I use this code:
var codeStatus = 'start';
function getStatus(){
return codeStatus;
}
function activities_upload(){
codeStatus = 'Started Execution';
...
codeStatus = 'Extracting Values';
...
codeStatus = 'Uploading Activities';
...
codeStatus = 'Complete';
}
It runs the required code, and even updates the #codestatus div with the first value, but it doesn't get any values beyond the first value. Additionally, it creates a continuous loop if there is an error in the code execution, so that isn't good either.
Is there a good, efficient, and safe way to complete this approach? Or, is there a better way to notify the user of the code execution status so they don't get worried if it takes a while, and can tell if there has been an issue?

I have struggled with this for some time. Unfortunately, I don't have a good fix for your approach, but I can show what I finally did and it seems to be working.
First, create an easy way to send a toast to your users.
function updateStatus_(alert,title){
var ui = SpreadsheetApp.getActiveSpreadsheet();
var title_ = title!=""?title:"";
ui.toast(alert,title_);
}
Second, as required, use the toast to update the user.
function activities_upload(){
updateStatus_('Started Execution');
...
updateStatus_('Extracting Values');
...
updateStatus_('Uploading Activities');
...
updateStatus_('Complete');
}
This will alert the user with a temporary message as the code progresses and not require the user to clear an alert.
Please note that if the steps progress rapidly the user will see the toast flash on the screen only to be quickly replaced by the next toast. So, make sure you don't have too many throughout your execution.

Related

Is it possible to show an element just before entering a long running sync process?

This is a very simple use case. Show an element (a loader), run some heavy calculations that eat up the thread and hide the loader when done. I am unable to get the loader to actually show up prior to starting the long running process. It ends up showing and hiding after the long running process. Is adding css classes an async process?
See my jsbin here:
http://jsbin.com/voreximapewo/12/edit?html,css,js,output
To explain what a few others have pointed out: This is due to how the browser queues the things that it needs to do (i.e. run JS, respond to UI events, update/repaint how the page looks etc.). When a JS function runs, it prevents all those other things from happening until the function returns.
Take for example:
function work() {
var arr = [];
for (var i = 0; i < 10000; i++) {
arr.push(i);
arr.join(',');
}
document.getElementsByTagName('div')[0].innerHTML = "done";
}
document.getElementsByTagName('button')[0].onclick = function() {
document.getElementsByTagName('div')[0].innerHTML = "thinking...";
work();
};
(http://jsfiddle.net/7bpzuLmp/)
Clicking the button here will change the innerHTML of the div, and then call work, which should take a second or two. And although the div's innerHTML has changed, the browser doesn't have chance to update how the actual page looks until the event handler has returned, which means waiting for work to finish. But by that time, the div's innerHTML has changed again, so that when the browser does get chance to repaint the page, it simply displays 'done' without displaying 'thinking...' at all.
We can, however, do this:
document.getElementsByTagName('button')[0].onclick = function() {
document.getElementsByTagName('div')[0].innerHTML = "thinking...";
setTimeout(work, 1);
};
(http://jsfiddle.net/7bpzuLmp/1/)
setTimeout works by putting a call to a given function at the back of the browser's queue after the given time has elapsed. The fact that it's placed at the back of the queue means that it'll be called after the browser has repainted the page (since the previous HTML changing statement would've queued up a repaint before setTimeout added work to the queue), and therefore the browser has had chance to display 'thinking...' before starting the time consuming work.
So, basically, use setTimeout.
let the current frame render and start the process after setTimeout(1).
alternatively you could query a property and force a repaint like this: element.clientWidth.
More as a what is possible answer you can make your calculations on a new thread using HTML5 Web Workers
This will not only make your loading icon appear but also keep it loading.
More info about web workers : http://www.html5rocks.com/en/tutorials/workers/basics/

innerHTML can't be trusted: Does not always execute synchronously

To see the problem in action, see this jsbin. Clicking on the button triggers the buttonHandler(), which looks like this:
function buttonHandler() {
var elm = document.getElementById("progress");
elm.innerHTML = "thinking";
longPrimeCalc();
}
You would expect that this code changes the text of the div to "thinking", and then runs longPrimeCalc(), an arithmetic function that takes a few seconds to complete. However, this is not what happens. Instead, "longPrimeCalc" completes first, and then the text is updated to "thinking" after it's done running, as if the order of the two lines of code were reversed.
It appears that the browser does not run "innerHTML" code synchronously, but instead creates a new thread for it that executes at its own leisure.
My questions:
What is happening under the hood that is leading to this behavior?
How can I get the browser to behave the way I would expect, that is, force it to update the "innerHTML" before it executes "longPrimeCalc()"?
I tested this in the latest version of chrome.
Your surmise is incorrect. The .innerHTML update does complete synchronously (and the browser most definitely does not create a new thread). The browser simply does not bother to update the window until your code is finished. If you were to interrogate the DOM in some way that required the view to be updated, then the browser would have no choice.
For example, right after you set the innerHTML, add this line:
var sz = elm.clientHeight; // whoops that's not it; hold on ...
edit — I might figure out a way to trick the browser, or it might be impossible; it's certainly true that launching your long computation in a separate event loop will make it work:
setTimeout(longPrimeCalc, 10); // not 0, at least not with Firefox!
A good lesson here is that browsers try hard not to do pointless re-flows of the page layout. If your code had gone off on a prime number vacation and then come back and updated the innerHTML again, the browser would have saved some pointless work. Even if it's not painting an updated layout, browsers still have to figure out what's happened to the DOM in order to provide consistent answers when things like element sizes and positions are interrogated.
I think the way it works is that the currently running code completes first, then all the page updates are done. In this case, calling longPrimeCalc causes more code to be executed, and only when it is done does the page update change.
To fix this you have to have the currently running code terminate, then start the calculation in another context. You can do that with setTimeout. I'm not sure if there's any other way besides that.
Here is a jsfiddle showing the behavior. You don't have to pass a callback to longPrimeCalc, you just have to create another function which does what you want with the return value. Essentially you want to defer the calculation to another "thread" of execution. Writing the code this way makes it obvious what you're doing (Updated again to make it potentially nicer):
function defer(f, callback) {
var proc = function() {
result = f();
if (callback) {
callback(result);
}
}
setTimeout(proc, 50);
}
function buttonHandler() {
var elm = document.getElementById("progress");
elm.innerHTML = "thinking...";
defer(longPrimeCalc, function (isPrime) {
if (isPrime) {
elm.innerHTML = "It was a prime!";
}
else {
elm.innerHTML = "It was not a prime =(";
}
});
}

Javascript - Show Spinner, Save Data Synchronously, Remove Spinner

I am trying to create a javascript plugin that allows you to hook into the save operation. I want some logic wrapped around the logic though:
Show a spinner
Execute save hook-in
Execute save logic
Remove Spinner
here is an example of my code:
function save(){
var settings = getSettings();
showSpinner();
// Hook-in call
if(settings.onSave)
settings.onSave();
saveState();
removeSpinner();
}
showSpinner() just hides save/cancel buttons and shows a spinner in its place.
hideSpinner() does the opposite.
saveState() saves the data from input fields to their data attributes.
settings.onSave() can be whatever code is passed in to the config by a user of the plugin.
When I run the process the spinner never shows up even though the hook-in I have defined takes ~1.5 seconds to return. If I pause the code in the debugger, the spinner does have a chance to show and it does disappear after the operation is complete.
What is the best approach to resolve this? I am not at all familiar with asynchronous design patterns within javascript. I know there is a single thread that the javascript can run on but I don't know why the save operation is getting in the way of showing the spinner.
Your browser won't update the interface until Javascript gives back control to the browser, meaning you need to return from function before the browser will show the spinner. Use setTimeout to give control back to the browser briefly.
function save(){
showSpinner();
setTimeout(function(){
var settings = getSettings();
// Hook-in call
if(settings.onSave)
settings.onSave();
saveState();
removeSpinner();
}, 0 );
}

javascript, while loop

i'm trying to get my script to wait for user input (click of a button) before continuing, this is v feasible in other languages, but seems impossible in js. basically, i want the user to select an option within a given time frame, if the user selects the wrong option, they're told..script then conts...otherwise, if after a certain amount of time theres no response...script just continues again sowing them the correct ans, but there seems to be nothing in js to make the script wait for that user input! ive tried a while loop, but that is just a big no no in js, ive used settimeout but has no real effect because the script just continues like normal then performs an action after x amount of time, ive tried setting variables and letting the script cont only if it is of a particular value, which is set only if the user clicks...eg var proceed=false, this is only set to true if the user clicks a button, but it still doesn't work... ive tried sooo many other solutions but nothing actually seems to be working. i like the idea of a while loop, because it doeas exactly what i want it to so, but if completly freezes my browser, is there a more effecient type of loop that will will peroform in the same manner with crashing my browser?
heres my code below that compltely freezes my computer. this method is called within a for loop which calls another method after it.
function getUserResp(){
$("#countdown").countdown({seconds: 15});
setTimeout("proceed=true", 16000);
$("#ans1").click(function(){
ansStr=$(this).text();
checkAns(ansStr);
});
$("#ans2").click(function(){
ansStr=$(this).text();
checkAns(ansStr);
});
$("#ans3").click(function(){
ansStr=$(this).text();
checkAns(ansStr);
});
would like something like this.....or just some sort of loop to make the script wait before going ahead so at least it gives the user some time to respond rather than running straight though!
do{
$(".ans").mouseover(function(){
$(this).addClass("hilite").fadeIn(800);
});
$(".ans").mouseout(function(){
$(this).removeClass("hilite");
});
}while(proceed==false);
}
You're doing it wrong.
JavaScript in the browser uses an event-driven model. There's no main function, just callbacks that are called when an event happens (such as document ready or anchor clicked). If you want something to happen after a user clicks something, then put a listener on that thing.
What you've done just keeps adding an event listener every time round the loop.
If you want to wait for user input then just don't do anything - the browser waits for user input (it's got an internal event loop). The worst thing you can do is try to reimplement your own event loop on top of the browser's.
You need to learn JavaScript. Trying to write JavaScript like you would another language only leads to pain and suffering. Seriously.
Douglas Crockford said it best:
JavaScript is a language that most people don’t bother to learn before they use. You can’t do that with any other language, and you shouldn’t want to, and you shouldn’t do that with this language either. Programming is a serious business, and you should have good knowledge about what you’re doing, but most people feel that they ought to be able to program in this language without any knowledge at all, and it still works. It’s because the language has enormous expressive power, and that’s not by accident.
You can't block the Javascript from running in the same way that you can in some other imperative languages. There's only one thread for Javascript in the browser, so if you hang it in a loop, nothing else can happen.
You must use asynchronous, event-driven programming. Setting a click handler (or whatever) combined with a timeout is the right way to start. Start a 15 second setTimeout. Inside the click handler for the answers, cancel the timeout. This way the timeout's handler only happens if the user doesn't click an answer.
For example:
var mytimeout = setTimeout(15000, function() {
// This is an anonymous function that will be called when the timer goes off.
alert("You didn't answer in time.");
// Remove the answer so the user can't click it anymore, etc...
$('#ans').hide();
});
$('#ans').click(function() {
// Clear the timeout, so it will never fire the function above.
clearTimeout(mytimeout);
alert("You picked an answer!");
});
See how the code must be structured such that it's event-driven. There's no way to structure it to say "do this thing, and wait here for an answer."
You're looking at client-side javascript as if it wasn't already in an event-driven loop. All you need to do is wait for the appropriate event to happen, and if it hasn't happened yet, continue to wait, or else perform some default action.
You don't need to:
create main loop: // All
wait for user input // Of
timer = start_timer() // This
// Is done for you
if [user has input data]:
process_data()
else if [timer > allowed_time]:
process_no_data()
else:
wait() // By the Browser
You only need the middle part. All you need to do is (Actual javascript follows, not pseudo-code):
// First, store all of the answer sections,
// so you're not grabbing them every time
// you need to check them.
var answers = {};
answers.ans1 = $("#ans1");
answers.ans2 = $("#ans2");
answers.ans3 = $("#ans3");
// This is a flag. We'll use it to check whether we:
// A. Have waited for 16 seconds
// B. Have correct user input
var clear_to_proceed = false;
var timer_id;
// Now we need to set up a function to check the answers.
function check_answers() {
if ( ! clear_to_proceed ) {
clear_to_proceed = checkAns(answers.ans1.text());
clear_to_proceed = checkAns(answers.ans2.text());
clear_to_proceed = checkAns(answers.ans3.text());
// I assume checkAns returns
// true if the answer is correct
// and false if it is wrong
}
if ( clear_to_proceed ) {
clearTimeout(timer_id);
return true; // Or do whatever needs be done,
// as the client has answered correctly
} else {
// If we haven't set a timer yet, set one
if ( typeof timer_id === 'undefined' ) {
timer_id = setTimeout(function(){
// After 16 seconds have passed we'll check their
// answers one more time and then force the default.
check_answers();
clear_to_proceed = true;
check_answers();
}, 16000);
}
return false; // We're just waiting for now.
}
}
// Finally, we check the answers any time the user interact
// with the answer elements.
$("#ans1,#ans2,#ans3").bind("focus blur", function() {
check_answers();
});

Thread Safety in Javascript?

I have a function called save(), this function gathers up all the inputs on the page, and performs an AJAX call to the server to save the state of the user's work.
save() is currently called when a user clicks the save button, or performs some other action which requires us to have the most current state on the server (generate a document from the page for example).
I am adding in the ability to auto save the user's work every so often. First I would like to prevent an AutoSave and a User generated save from running at the same time. So we have the following code (I am cutting most of the code and this is not a 1:1 but should be enough to get the idea across):
var isSaving=false;
var timeoutId;
var timeoutInterval=300000;
function save(showMsg)
{
//Don't save if we are already saving.
if (isSaving)
{
return;
}
isSaving=true;
//disables the autoSave timer so if we are saving via some other method
//we won't kick off the timer.
disableAutoSave();
if (showMsg) { //show a saving popup}
params=CollectParams();
PerformCallBack(params,endSave,endSaveError);
}
function endSave()
{
isSaving=false;
//hides popup if it's visible
//Turns auto saving back on so we save x milliseconds after the last save.
enableAutoSave();
}
function endSaveError()
{
alert("Ooops");
endSave();
}
function enableAutoSave()
{
timeoutId=setTimeOut(function(){save(false);},timeoutInterval);
}
function disableAutoSave()
{
cancelTimeOut(timeoutId);
}
My question is if this code is safe? Do the major browsers allow only a single thread to execute at a time?
One thought I had is it would be worse for the user to click save and get no response because we are autosaving (And I know how to modify the code to handle this). Anyone see any other issues here?
JavaScript in browsers is single threaded. You will only ever be in one function at any point in time. Functions will complete before the next one is entered. You can count on this behavior, so if you are in your save() function, you will never enter it again until the current one has finished.
Where this sometimes gets confusing (and yet remains true) is when you have asynchronous server requests (or setTimeouts or setIntervals), because then it feels like your functions are being interleaved. They're not.
In your case, while two save() calls will not overlap each other, your auto-save and user save could occur back-to-back.
If you just want a save to happen at least every x seconds, you can do a setInterval on your save function and forget about it. I don't see a need for the isSaving flag.
I think your code could be simplified a lot:
var intervalTime = 300000;
var intervalId = setInterval("save('my message')", intervalTime);
function save(showMsg)
{
if (showMsg) { //show a saving popup}
params=CollectParams();
PerformCallBack(params, endSave, endSaveError);
// You could even reset your interval now that you know we just saved.
// Of course, you'll need to know it was a successful save.
// Doing this will prevent the user clicking save only to have another
// save bump them in the face right away because an interval comes up.
clearInterval(intervalId);
intervalId = setInterval("save('my message')", intervalTime);
}
function endSave()
{
// no need for this method
alert("I'm done saving!");
}
function endSaveError()
{
alert("Ooops");
endSave();
}
All major browsers only support one javascript thread (unless you use web workers) on a page.
XHR requests can be asynchronous, though. But as long as you disable the ability to save until the current request to save returns, everything should work out just fine.
My only suggestion, is to make sure you indicate to the user somehow when an autosave occurs (disable the save button, etc).
All the major browsers currently single-thread javascript execution (just don't use web workers since a few browsers support this technique!), so this approach is safe.
For a bunch of references, see Is JavaScript Multithreaded?
Looks safe to me. Javascript is single threaded (unless you are using webworkers)
Its not quite on topic but this post by John Resig covers javascript threading and timers:
http://ejohn.org/blog/how-javascript-timers-work/
I think the way you're handling it is best for your situation. By using the flag you're guaranteeing that the asynchronous calls aren't overlapping. I've had to deal with asynchronous calls to the server as well and also used some sort of flag to prevent overlap.
As others have already pointed out JavaScript is single threaded, but asynchronous calls can be tricky if you're expecting things to say the same or not happen during the round trip to the server.
One thing, though, is that I don't think you actually need to disable the auto-save. If the auto-save tries to happen when a user is saving then the save method will simply return and nothing will happen. On the other hand you're needlessly disabling and reenabling the autosave every time autosave is activated. I'd recommend changing to setInterval and then forgetting about it.
Also, I'm a stickler for minimizing global variables. I'd probably refactor your code like this:
var saveWork = (function() {
var isSaving=false;
var timeoutId;
var timeoutInterval=300000;
function endSave() {
isSaving=false;
//hides popup if it's visible
}
function endSaveError() {
alert("Ooops");
endSave();
}
function _save(showMsg) {
//Don't save if we are already saving.
if (isSaving)
{
return;
}
isSaving=true;
if (showMsg) { //show a saving popup}
params=CollectParams();
PerformCallBack(params,endSave,endSaveError);
}
return {
save: function(showMsg) { _save(showMsg); },
enableAutoSave: function() {
timeoutId=setInterval(function(){_save(false);},timeoutInterval);
},
disableAutoSave: function() {
cancelTimeOut(timeoutId);
}
};
})();
You don't have to refactor it like that, of course, but like I said, I like to minimize globals. The important thing is that the whole thing should work without disabling and reenabling autosave every time you save.
Edit: Forgot had to create a private save function to be able to reference from enableAutoSave

Categories

Resources