Throw custom timeout exception - javascript

I have a Google Apps Script web app ("Web App") that executes as the user, then calls individual functions from another Apps Script project ("API Executable") via the Apps Script API using UrlFetchApp.fetch() and executes them as me (see Get user info when someone runs Google Apps Script web app as me).
A limitation of this method is that UrlFetchApp.fetch() has a 60s timeout, and one of my functions often takes longer than this. The API Executable function finishes running successfully, but the web app throws a timeout exception. I would like to handle this exception by running a second "follow-up" function that finds and returns the URL of the Google Sheet successfully created by the original function. However, I'll need to pass the follow-up function one of the parameters passed to the original function, and it appears I can't do this within a standard try...catch block.
My idea was to throw an exception that contains the needed parameter, but I can't figure out how to throw my own timeout exception; since Google Apps Script is synchronous, there's no way to track how long UrlFetchApp.fetch() has been running while it's running.
Is there a way to throw your own timeout exception? Or, is there another way I can pass the needed parameter to a function that executes if there's a timeout error?
I tagged Javascript in this post as well since there's a lot of overlap with Google Apps Script and I figured it would improve my chance of connecting with someone who has an answer--hope that's okay. Below is the function I'm using in my web app to call my API Executable functions, in case that's helpful.
EDIT: Based on #TheMaster's comment, I decided to write the script as though parameters passed to executeAsMe() WERE being passed to the catch() block, to see what happened. I expected an exception regarding the fact the opt_timeoutFunction was undefined, but strangely it looks like only the first line of the catch() block is even running, and I'm not sure why.
function executeAsMe(functionName, paramsArray, opt_timeoutFunction, opt_timeoutParams) {
try {
console.log('Using Apps Script API to call function ' + functionName.toString() + ' with parameter(s) ' + paramsArray.toString());
var url = 'https://script.googleapis.com/v1/scripts/Mq71nLXJPX95eVDFPW2DJzcB61X_XfA8E:run';
var payload = JSON.stringify({"function": functionName, "parameters": paramsArray, "devMode": true})
var params = {method:"POST",
headers: {Authorization: 'Bearer ' + getAppsScriptService().getAccessToken()},
payload:payload,
contentType:"application/json",
muteHttpExceptions:true};
var results = UrlFetchApp.fetch(url, params);
var jsonResponse = JSON.parse(results).response;
if (jsonResponse == undefined) {
var jsonResults = undefined;
} else {
var jsonResults = jsonResponse.result;
}
} catch(error) {
console.log('error = ' + error); // I'm seeing this in the logs...
console.log('error.indexOf("Timeout") = ' + error.indexOf("Timeout").toString); // ...but not this. It skips straight to the finally block
if (error.indexOf('Timeout') > 0) { // If error is a timeout error, call follow-up function
console.log('Using Apps Script API to call follow-up function ' + opt_timeoutFunction.toString() + ' with parameter(s) ' + paramsArray.toString());
var url = 'https://script.googleapis.com/v1/scripts/Mq71nLXJPX95eVDFPW2DJzcB61X_XfA8E:run';
var payload = JSON.stringify({"function": opt_timeoutFunction, "parameters": opt_timeoutParams, "devMode": true})
var params = {method:"POST",
headers: {Authorization: 'Bearer ' + getAppsScriptService().getAccessToken()},
payload:payload,
contentType:"application/json",
muteHttpExceptions:true};
var results = UrlFetchApp.fetch(url, params);
var jsonResponse = JSON.parse(results).response;
if (jsonResponse == undefined) {
var jsonResults = undefined;
} else {
var jsonResults = jsonResponse.result;
}
}
} finally {
console.log('jsonResults = ' + jsonResults);
return jsonResults;
}
}

I ended up using the '''catch()''' block to throw an exception back to the client side and handle it there.
Google Apps Script:
function executeAsMe(functionName, paramsArray) {
try {
console.log('Using Apps Script API to call function ' + functionName.toString() + ' with parameter(s) ' + paramsArray.toString());
var url = 'https://script.googleapis.com/v1/scripts/Mq71nLXJPX95eVDFPW2DJzcB61X_XfA8E:run';
var payload = JSON.stringify({"function": functionName, "parameters": paramsArray, "devMode": true})
var params = {method:"POST",
headers: {Authorization: 'Bearer ' + getAppsScriptService().getAccessToken()},
payload:payload,
contentType:"application/json",
muteHttpExceptions:true};
var results = UrlFetchApp.fetch(url, params);
var jsonResponse = JSON.parse(results).response;
if (jsonResponse == undefined) {
var jsonResults = undefined;
} else {
var jsonResults = jsonResponse.result;
}
return jsonResults;
} catch(error) {
console.log('error = ' + error);
if (error.toString().indexOf('Timeout') > 0) {
console.log('Throwing new error');
throw new Error('timeout');
} else {
throw new Error('unknown');
}
} finally {
}
}
Client-side Javascript (a simplified version):
function createMcs() {
var userFolder = getDataFromHtml().userFolder;
google.script.run
.withSuccessHandler(createMcsSuccess)
.withFailureHandler(createMcsFailure)
.withUserObject(userFolder)
.executeAsMe('createMasterCombinedSchedule', [userFolder]);
}
function createMcsSuccess(mcsParameter) {
if (mcsParameter == undefined) {
simpleErrorModal.style.display = "block"; // A generic error message
} else {
document.getElementById("simpleAlertHeaderDiv").innerHTML = 'Master Combined Schedule Created Successfully';
document.getElementById("simpleAlertBodyDiv").innerHTML = 'Your Master Combined Schedule was created successfully. Click here to view.';
simpleAlertModal.style.display = "block";
}
}
function createMcsFailure(mcsError, userFolder, counter) { // The exception I threw will activate this function
if (!counter) { // Added a counter to increment every time checkForCreatedMcs() runs so it doesn't run indefinitely
var counter = 0;
}
if (mcsError.message == 'Error: timeout' && counter < 5) { // If timeout error, wait 10s and look for MCS URL
window.setTimeout(checkForCreatedMcs(mcsError, userFolder, counter), 10000);
} else if (mcsError.message == 'Error: timeout' && counter == 5) { // If it hasn't worked after 5 tries, show generic error message
simpleErrorModal.style.display = "block";
} else { // For any error that's not a timeout exception, show generic error message
simpleErrorModal.style.display = "block";
}
}
function checkForCreatedMcs(mcsError, userFolder, counter) {
counter++;
google.script.run
.withSuccessHandler(checkForCreatedMcsSuccess)
.withUserObject([mcsError, userFolder, counter])
.executeAsMe('checkIfMcsExists', [userFolder]); // checkIfMcsExists() is a pre-existing function in my API Executable project I had already used elsewhere
}
function checkForCreatedMcsSuccess(mcsExistsParameter, params) {
var mcsError = params[0];
var userFolder = params[1];
var counter = params[2];
if (mcsExistsParameter == undefined) { // If function returns undefined, show generic error message
simpleErrorModal.style.display = "block";
} else if (mcsExistsParameter == false) { // If function returns false, wait 10s and try again
createMcsFailure(mcsError, userFolder, counter);
} else { // If function returns URL, show success modal with link
document.getElementById("simpleAlertHeaderDiv").innerHTML = 'Master Combined Schedule Created Successfully';
document.getElementById("simpleAlertBodyDiv").innerHTML = 'Your Master Combined Schedule was created successfully. Click here to view.';
simpleAlertModal.style.display = "block";
}
}
I am sure there has to be a tidier/less complex way to do this, but this worked!

Related

Checking if image exists produces 403 error

I have a range of hosted images, but the amount and url can change. However, they always follow the same incrementing naming convention. In order to store the images that exist, I loop through the available images until I encounter an error. At that point I store the results and continue with the operations.
function checkForExistingImage(u, r = [], i = 0){
var url = u + i;
$.get(url).done(function() {
r.push(url);
checkForExistingImage(u, r, i + 1);
}).fail(function(){
//continue working with the 'r' array
});
}
However, this will always result in a 403 (Image not found) error in the console, because the last image checked will never exist.
How can I not trigger this error, or maybe suppress it if needed?
I would definitely rewrite this in a more civilised manner. This function does not log any errors outside of those that are explicitly logged by console.log (which you can remove).
It should be safer to use this as it does not bombard the server with too many requests per second, but you can reduce or remove the timeout if that's not a problem.
function Timeout(time) {
return new Promise(function(resolve) {setTimeout(resolve, time);});
}
async function checkForExistingImages(baseUrl, maxImages, startImage = 0) {
const results = [];
// Some sanity check on params
if(maxImages < startImage) {
let tmp = maxImages;
maxImages = startImage + 1;
startImage = maxImages;
}
// from i to max
for(let i=startImage; i<maxImages; ++i) {
// Create image URL, change this as needed
const imageURL = baseUrl + i + ".png";
// `fetch` does not throw usually, but we wanted to avoid errors in console
try {
// Response will have 200/404/403 or something else
const response = await fetch(imageURL);
if(response.status == 200) {
results.push(imageURL);
}
else {
console.info("Image",imageURL,"was removed.");
// stop loading
break;
}
}
// complete failure, some major error occured
catch(e) {
console.warn("Image",imageURL, "failed to send request!");
break;
}
// If you're getting throttled by the server, use timeout
await Timeout(200);
}
return results;
}

I'm Building an auto dialer with Twilio.Device Js

I'm building an auto dialer with twilio and I'm a litle bit stuck . I succed with getting 2 outbound calls from my browser and when the first hangs up or busy the second one starts . but I want to implement it for many calls from an array of phones in my sql Db .
here is my code - I'm starting with connecting to twilio and getting a token after that I'm making an array of phones using an AJAX call to my db with php code , Then I'm making a $GetJson request for the first phone and the next phone (if exist) from the Db and finally I'm starting a twilio.device.connect and when the first disconnect the second's starts:
status_progress.php
/********TWILIO **********/
$.getJSON('../../twilio/token.php').done(function (data) {
console.log('Token: ' + data.token);
console.log('Identity: ' + data.identity);
// Setup Twilio.Device
Twilio.Device.setup(data.token);
Twilio.Device.ready(function (device) {
console.log('Twilio.Device Ready!');
});
Twilio.Device.error(function (error) {
console.log('Twilio.Device Error: ' + error.message);
});
Twilio.Device.connect(function (conn) {
console.log('Successfully established call!');
});
Twilio.Device.disconnect(function (conn) {
console.log('Call ended.');
});
Twilio.Device.incoming(function (conn) {
console.log('Incoming connection from ' + conn.parameters.From);
var archEnemyPhoneNumber = '+12099517118';
if (conn.parameters.From === archEnemyPhoneNumber) {
conn.reject();
console.log('It\'s your nemesis. Rejected call.');
} else {
// accept the incoming connection and start two-way audio
conn.accept();
}
});
//setClientNameUI(data.identity);
})
.fail(function () {
console.log('Could not get a token from server!');
});
// Bind button to hangup call
document.getElementById('closeCall').onclick = function () {
console.log('Hanging up...');
Twilio.Device.disconnectAll();
};
$("#phoneThem").live("click", function() {
var arr = [];
var i =0;
$('.checkDeal:checked').each(function () {
arr[i++] = $(this).val();
});
var arr_value = JSON.stringify(arr);
var conected = 'no';
$.ajax({
type: "POST",
url: 'make_call_list.php',
data: {arr_checked : arr_value}, // serializes the form's elements.
success: function(calls)
{
$.getJSON('make_a_call.php').done(function (data) {
var arr = data.arr;
var params = {
To: data.phone + '-' + data.crm
};
console.log('Calling ' + params.To + '...' );
var connection = Twilio.Device.connect(params);
connection.on('disconnect', function(conn) {
console.log("the call has ended");
var params2 = {
To: data.phone_next + '-' + data.crm
};
console.log('Calling ' + params2.To + '...' );
var connection_2 = Twilio.Device.connect(params2);
});
});
}, // SUCCES MAKE A CALL LIST
});
});
this is the twiml code :
<?php
require_once 'twilio-php-master/Twilio/autoload.php';
use Twilio\TwiML\VoiceResponse;
$break = $_POST['To'];
$break_arr = explode('-',$break);
$prefix = '+972';
$response = new VoiceResponse();
$dial = $response->dial('', ['callerId' => '+972 50-225-8234']);
$break_arr[0] = ltrim($break_arr[0] , '0');
$break_arr[0] = $prefix.$break_arr[0] ;
$dial->number($break_arr[0], ['statusCallbackEvent' => 'initiated
ringing answered completed',
'statusCallback' =>
'https://crm.unic.co.il/crm_test_zone/handleDialCallStatus.php?
crm='.$break_arr[1].'',
'statusCallbackMethod' => 'POST']);
echo $response;
?>
Twilio developer evangelist here.
Rather than write out the code to sequentially dial all of the numbers, you can use the callback functions within the Twilio Device to trigger the next call to happen.
What I would do is load all of the numbers you want to dial into the front end in an array.
Then, I would create a function that takes two arguments; the current index of the number you are dialling and the array of numbers. The function would check to make sure the current index is still within the array and if it isn't return (all numbers have been dialled). Otherwise, it would generate a call to the number in the array. It would also set up a listener for the disconnection event for that call. When it disconnects, it calls the same function again, but moves the index on 1.
Finally, call that function with the initial index of 0 and the array of numbers. Something a bit like this:
const array = getArrayOfNumbers();
function dialNumber(index, array) {
if (index > array.length) return;
const connection = Twilio.Device.connect({ number: array[index] });
connection.on('disconnected', function() {
dialNumber(index+1, array);
});
}
dialNumber(0, array);
Let me know if that helps at all.

Know when jqXHRs of an array are all completed

I'm trying to run some code once all the jqXHR elements of an array are completed (have either succeeded or failed).
You can see the full code here: http://jsfiddle.net/Lkjcrdtz/4/
Basically I'm expecting the always hook from here:
$.when
.apply(undefined, reqs)
.always(function(data) {
console.log('ALL ALWAYS', data);
});
to run when all the requests that were piled up there have either succeeded or failed. Currently, you can observe in the console that ALL ALWAYS is logged earlier.
A simple solution for modern browsers would be to use the newer fetch() API along with Promise.all()
var makeReq = function(url, pos) {
var finalUrl = url + pos;
// intentionally make this request a failed one
if (pos % 2 === 0) {
finalUrl = "https://jsonplaceholder.typicode.com/423423rfvzdsv";
}
return fetch(finalUrl).then(function(resp) {
console.log('Request for user #', pos);
// if successful request return data promise otherwise return something
// that can be used to filter out in final results
return resp.ok ? resp.json() : {error: true, status: resp.status, id: pos }
})
};
// mock API
var theUrl = "https://jsonplaceholder.typicode.com/users/";
var reqs = [];
for (var i = 1; i <= 5; i++) {
reqs.push(makeReq(theUrl, i));
}
Promise.all(reqs).then(function(results) {
console.log('---- ALL DONE ----')
// filter out good requests
results.forEach(function(o) {
if (o.error) {
console.log(`No results for user #${o.id}`);
} else {
console.log(`User #${o.id} name = ${o.name}`);
}
})
})

Javascript - Google API GET: Value null outside google api call

I am making a call to Google Calendar API for a specific event and am able to get the recurrence value.
function getRecurrence(payload) {
console.log('payload = ' + payload);
var recurrence = '';
if (payload != undefined) {
var requestRecurringEvent = window.gapi.client.calendar.events.get({
'calendarId': 'primary',
'eventId': payload
});
requestRecurringEvent.execute(function(resp) {
console.log('requestRecurringEvent = ' + resp);
console.log('requestRecurringEvent.recurrence = ' + resp.recurrence);
recurrence = resp.recurrence;
console.log('recurrence (inside execute)= ' + recurrence); //NO ISSUE (YET): recurrence (inside execute) = RRULE:FREQ=WEEKLY;COUNT=10
return recurrence;
});
} else {
return 'no recurrence value';
}
}
However, when I return the recurrence value to a variable
var recurrence = getRecurrence(action.payload.calendarEventRecurringEventId);
console.log('recurrence (outside execute) = ' + recurrence); //ISSUE: recurrence (outside execute) = undefined
note: action.payload.calendarEventRecurringEventId is because I am passing in the value from payload in redux.
Why is the value defined inside the api call but undefined outside the api call??
Thanks!
UPDATE
Promises seem to have taken me in the right direction, since the console.log('recurrence (outside execute) = ' + recurrence) prints a value, but it seems to not have completely solved the problem since an empty string is still being set for the value in redux.
var recurrencePromise = new Promise(function(resolve, reject) {
var payload = action.payload.calendarEventRecurringEventId;
console.log('action.payload.calendarEventRecurringEventId = ' + payload);
var recurrence = '';
if (payload != undefined) {
console.log('getRecurrence payload != undefined');
var requestRecurringEvent = window.gapi.client.calendar.events.get({
'calendarId': 'primary',
'eventId': payload
});
requestRecurringEvent.execute(function(resp) {
console.log('requestRecurringEvent = ' + resp);
console.log('requestRecurringEvent.recurrence = ' + resp.recurrence);
recurrence = resp.recurrence;
console.log('recurrence (inside execute)= ' + recurrence);
resolve(recurrence);
});
} else {
reject(Error("It broke"));
}
});
recurrencePromise.then(function(recurrence) {
console.log('recurrence (outside execute) = ' + recurrence);
var recurrenceTemp = 'RRULE:FREQ=WEEKLY;COUNT=10';
return {
...state,
calendarEventEndDate: action.payload.calendarEventEndDate,
calendarEventEndDateTime: action.payload.calendarEventEndDateTime,
calendarEventEndTime: action.payload.calendarEventEndTime,
calendarEventID: action.payload.calendarEventID,
calendarEventTitle: action.payload.calendarEventTitle,
calendarEventRecurringEventId: defaultIfUndefined(action.payload.calendarEventRecurringEventId, ''),
calendarEventRecurrence: recurrence, //ISSUE: this is where the value should be something like `RRULE:FREQ=WEEKLY;COUNT=10` instead of `''`
}
}, function(err) {
console.log(err); // Error: "It broke"
});
Why is the right value still not set defined outside the api call?
Because the requestRecurringEvent.execute function is asynchronous, which means you can never tell when the asynchronous function completes its execution (returns) unlike a synchronous function that guaranties a return value, async doesnt. What you need to do if you want to use the recurrence value outside is, A) Use a callback function B) use Promises (which I am sure the google api call provides by default)
I recommend you read more about asynchronity in javascript, to have a grasp on how it works.

This code doesn't seem to fire in order?

My problem is that the code does not seem to be running in order, as seen below.
This code is for my discord.js bot that I am creating.
var Discord = require("discord.js");
var bot = new Discord.Client();
var yt = require("C:/Users/username/Documents/Coding/Discord/youtubetest.js");
var youtubetest = new yt();
var fs = require('fs');
var youtubedl = require('youtube-dl');
var prefix = "!";
var vidid;
var commands = {
play: {
name: "!play ",
fnc: "Gets a Youtube video matching given tags.",
process: function(msg, query) {
youtubetest.respond(query, msg);
var vidid = youtubetest.vidid;
console.log(typeof(vidid) + " + " + vidid);
console.log("3");
}
}
};
bot.on('ready', () => {
console.log('I am ready!');
});
bot.on("message", msg => {
if(!msg.content.startsWith(prefix) || msg.author.bot || (msg.author.id === bot.user.id)) return;
var cmdraw = msg.content.split(" ")[0].substring(1).toLowerCase();
var query = msg.content.split("!")[1];
var cmd = commands[cmdraw];
if (cmd) {
var res = cmd.process(msg, query, bot);
if (res) {
msg.channel.sendMessage(res);
}
} else {
let msgs = [];
msgs.push(msg.content + " is not a valid command.");
msgs.push(" ");
msgs.push("Available commands:");
msgs.push(" ");
msg.channel.sendMessage(msgs);
msg.channel.sendMessage(commands.help.process(msg));
}
});
bot.on('error', e => { console.error(e); });
bot.login("mytoken");
The youtubetest.js file:
var youtube_node = require('youtube-node');
var ConfigFile = require("C:/Users/username/Documents/Coding/Discord/json_config.json");
var mybot = require("C:/Users/username/Documents/Coding/Discord/mybot.js");
function myyt () {
this.youtube = new youtube_node();
this.youtube.setKey(ConfigFile.youtube_api_key);
this.vidid = "";
}
myyt.prototype.respond = function(query, msg) {
this.youtube.search(query, 1, function(error, result) {
if (error) {
msg.channel.sendMessage("There was an error finding requested video.");
} else {
vidid = 'http://www.youtube.com/watch?v=' + result.items[0].id.videoId;
myyt.vidid = vidid;
console.log("1");
}
});
console.log("2");
};
module.exports = myyt;
As the code shows, i have an object for the commands that the bot will be able to process, and I have a function to run said commands when a message is received.
Throughout the code you can see that I have put three console.logs with 1, 2 and 3 showing in which order I expect the parts of the code to run. When the code is run and a query is found the output is this:
I am ready!
string +
2
3
1
This shows that the code is running in the wrong order that I expect it to.
All help is very highly appreciated :)
*Update! Thank you all very much to understand why it isn't working. I found a solution where in the main file at vidid = youtubetest.respond(query, msg) when it does that the variable is not assigned until the function is done so it goes onto the rest of my code without the variable. To fix I simply put an if statement checking if the variable if undefined and waiting until it is defined.*
Like is mentioned before, a lot of stuff in javascript runs in async, hence the callback handlers. The reason it runs in async, is to avoid the rest of your code being "blocked" by remote calls. To avoid ending up in callback hell, most of us Javascript developers are moving more and more over to Promises. So your code could then look more like this:
myyt.prototype.respond = function(query, msg) {
return new Promise(function(resolve, reject) {
this.youtube.search(query, 1, function(error, result) {
if (error) {
reject("There was an error finding requested video."); // passed down to the ".catch" statement below
} else {
vidid = 'http://www.youtube.com/watch?v=' + result.items[0].id.videoId;
myyt.vidid = vidid;
console.log("1");
resolve(2); // Resolve marks the promises as successfully completed, and passes along to the ".then" method
}
});
}).then(function(two) {
// video is now the same as myyt.vidid as above.
console.log(two);
}).catch(function(err) {
// err contains the error object from above
msg.channel.sendMessage(err);
})
};
This would naturally require a change in anything that uses this process, but creating your own prototypes seems.. odd.
This promise returns the vidid, so you'd then set vidid = youtubetest.response(query, msg);, and whenever that function gets called, you do:
vidid.then(function(id) {
// id is now the vidid.
});
Javascript runs async by design, and trying to hack your way around that leads you to dark places fast. As far as I can tell, you're also targetting nodeJS, which means that once you start running something synchronously, you'll kill off performance for other users, as everyone has to wait for that sync call to finish.
Some suggested reading:
http://callbackhell.com/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
https://stackoverflow.com/a/11233849/3646975
I'd also suggest looking up ES6 syntax, as it shortens your code and makes life a hellofalot easier (native promises were only introduced in ES6, which NodeJS 4 and above supports (more or less))
In javascript, please remember that any callback function you pass to some other function is called asynchronously. I.e. the calls to callback function may not happen "in order". "In order" in this case means the order they appear on the source file.
The callback function is simply called on certain event:
When there is data to be processed
on error
in your case for example when the youtube search results are ready,
'ready' event is received or 'message' is received.
etc.

Categories

Resources