I've been digging around, and I'm not able to find references or documentation on how I can use Asynchronous Functions in Google App Script, I found that people mention It's possible, but not mention how...
Could someone point me in the right direction or provide me with an example?
Promises, Callbacks, or something, that can help me with this.
I have this function lets call it foo that takes a while to execute (long enough that It could time out an HTTP call).
What I'm trying to do Is to refactor it, in a way that it works like this:
function doPost(e) {
// parsing and getting values from e
var returnable = foo(par1, par2, par3);
return ContentService
.createTextOutput(JSON.stringify(returnable))
.setMimeType(ContentService.MimeType.JSON);
}
function foo(par1, par2, par3) {
var returnable = something(par1, par2, par3); // get the value I need to return;
// continue in an Async way, or schedule execution for something else
// and allow the function to continue its flow
/* async bar(); */
return returnable;
}
Now I want to realize that bit in foo because It takes to long and I don't want to risk for a time out, also the logic that occurs there it's totally client Independent, so It doesn't matter, I just need the return value, that I'll be getting before.
Also, I think It's worth mentioning that this is deployed in Google Drive as a web app.
It's been long since this, but adding some context, at that moment I wanted to scheduled several things to happen on Google Drive, and It was timing out the execution, so I was looking for a way to safely schedule a job.
You want to execute functions by the asynchronous processing using Google Apps Script.
You want to run the functions with the asynchronous processing using time trigger.
If my understanding is correct, unfortunately, there are no methods and the official document for directly achieving it. But as a workaround, that can be achieved by using both Google Apps Script API and the fetchAll method which can work by asynchronous processing.
The flow of this workaround is as follows.
Deploy API executable, enable Google Apps Script API.
Using fetchAll, request the endpoint of Google Apps Script API for running function.
When several functions are requested once, those work with the asynchronous processing by fetchAll.
Note:
I think that Web Apps can be also used instead of Google Apps Script API.
In order to simply use this workaround, I have created a GAS library. I think that you can also use it.
In this workaround, you can also run the functions with the asynchronous processing using time trigger.
References:
fetchAll
Deploy the script as an API executable
scripts.run of Google Apps Script API
Benchmark: fetchAll method in UrlFetch service for Google Apps Script
GAS library for running the asynchronous processing
If I misunderstand your question, I'm sorry.
There is another way to accomplish this.
You can use time-based one-off triggers to run functions asynchronously, they take a bit of time to queue up (30-60 seconds) but it is ideal for slow-running tasks that you want to remove from the main execution of your script.
// Creates a trigger that will run a second later
ScriptApp.newTrigger("myFunction")
.timeBased()
.after(1)
.create();
There is handy script that I put together called Async.gs that will help remove the boilerplate out of this technique. You can even use it to pass arguments via the CacheService.
Here is the link:
https://gist.github.com/sdesalas/2972f8647897d5481fd8e01f03122805
// Define async function
function runSlowTask(user_id, is_active) {
console.log('runSlowTask()', { user_id: user_id, is_active: is_active });
Utilities.sleep(5000);
console.log('runSlowTask() - FINISHED!')
}
// Run function asynchronously
Async.call('runSlowTask');
// Run function asynchronously with one argument
Async.call('runSlowTask', 51291);
// Run function asynchronously with multiple argument
Async.call('runSlowTask', 51291, true);
// Run function asynchronously with an array of arguments
Async.apply('runSlowTask', [51291, true]);
// Run function in library asynchronously with one argument
Async.call('MyLibrary.runSlowTask', 51291);
// Run function in library asynchronously with an array of arguments
Async.apply('MyLibrary.runSlowTask', [51291, true]);
With the new V8 runtime, it is now possible to write async functions and use promises in your app script.
Even triggers can be declared async! For example (typescript):
async function onOpen(e: GoogleAppsScript.Events.SheetsOnOpen) {
console.log("I am inside a promise");
// do your await stuff here or make more async calls
}
To start using the new runtime, just follow this guide. In short, it all boils down to adding the following line to your appsscript.json file:
{
...
"runtimeVersion": "V8"
}
Based on Tanaike's answer, I created another version of it. My goals were:
Easy to maintain
Easy to call (simple call convention)
tasks.gs
class TasksNamespace {
constructor() {
this.webAppDevUrl = 'https://script.google.com/macros/s/<your web app's dev id>/dev';
this.accessToken = ScriptApp.getOAuthToken();
}
// send all requests
all(requests) {
return requests
.map(r => ({
muteHttpExceptions: true,
url: this.webAppDevUrl,
method: 'POST',
contentType: 'application/json',
payload: {
functionName: r.first(),
arguments: r.removeFirst()
}.toJson(),
headers: {
Authorization: 'Bearer ' + this.accessToken
}
}), this)
.fetchAll()
.map(r => r.getContentText().toObject())
}
// send all responses
process(request) {
return ContentService
.createTextOutput(
request
.postData
.contents
.toObject()
.using(This => ({
...This,
result: (() => {
try {
return eval(This.functionName).apply(eval(This.functionName.splitOffLast()), This.arguments) // this could cause an error
}
catch(error) {
return error;
}
})()
}))
.toJson()
)
.setMimeType(ContentService.MimeType.JSON)
}
}
helpers.gs
// array prototype
Array.prototype.fetchAll = function() {
return UrlFetchApp.fetchAll(this);
}
Array.prototype.first = function() {
return this[0];
}
Array.prototype.removeFirst = function() {
this.shift();
return this;
}
Array.prototype.removeLast = function() {
this.pop();
return this;
}
// string prototype
String.prototype.blankToUndefined = function(search) {
return this.isBlank() ? undefined : this;
};
String.prototype.isBlank = function() {
return this.trim().length == 0;
}
String.prototype.splitOffLast = function(delimiter = '.') {
return this.split(delimiter).removeLast().join(delimiter).blankToUndefined();
}
// To Object - if string is Json
String.prototype.toObject = function() {
if(this.isBlank())
return {};
return JSON.parse(this, App.Strings.parseDate);
}
// object prototype
Object.prototype.toJson = function() {
return JSON.stringify(this);
}
Object.prototype.using = function(func) {
return func.call(this, this);
}
http.handler.gs
function doPost(request) {
return new TasksNamespace.process(request);
}
calling convention
Just make arrays with the full function name and the rest are the function's arguments. It will return when everything is done, so it's like Promise.all()
var a = new TasksNamespace.all([
["App.Data.Firebase.Properties.getById",'T006DB4'],
["App.Data.External.CISC.Properties.getById",'T00A21F', true, 12],
["App.Maps.geoCode",'T022D62', false]
])
return preview
[ { functionName: 'App.Data.Firebase.Properties.getById',
arguments: [ 'T006DB4' ],
result:
{ Id: '',
Listings: [Object],
Pages: [Object],
TempId: 'T006DB4',
Workflow: [Object] } },
...
]
Notes
it can handle any static method, any method off a root object's tree, or any root (global) function.
it can handle 0 or more (any number) of arguments of any kind
it handles errors by returning the error from any post
// First create a trigger which will run after some time
ScriptApp.newTrigger("createAsyncJob").timeBased().after(6000).create();
/* The trigger will execute and first delete trigger itself using deleteTrigger method and trigger unique id. (Reason: There are limits on trigger which you can create therefore it safe bet to delete it.)
Then it will call the function which you want to execute.
*/
function createAsyncJob(e) {
deleteTrigger(e.triggerUid);
createJobsTrigger();
}
/* This function will get all trigger from project and search the specific trigger UID and delete it.
*/
function deleteTrigger(triggerUid) {
let triggers = ScriptApp.getProjectTriggers();
triggers.forEach(trigger => {
if (trigger.getUniqueId() == triggerUid) {
ScriptApp.deleteTrigger(trigger);
}
});
}
While this isn't quite an answer to your question, this could lead to an answer if implemented.
I have submitted a feature request to Google to modify the implementation of doGet() and doPost() to instead accept a completion block in the functions' parameters that we would call with our response object, allowing additional slow-running logic to be executed after the response has been "returned".
If you'd like this functionality, please star the issue here: https://issuetracker.google.com/issues/231411987?pli=1
So I have this code guys
Template.mainLayout.onCreated(function () { //HERE
console.log("mainLayout created");
var context = FlowRouter.current();
// use context to access the URL state
console.log(context);
var visitedOne = context.path;
//getting the connID
var clientIp = headers.getClientIP(); // no need for this anymore
var clientConnId = Meteor.connection._lastSessionId; // HERE
console.log(clientIp);
console.log(clientConnId); //HERE
// console.log(Meteor.connection._lastSessionId);
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, function(error, result){
if(error){
console.log("error", error);
});
if(result){
}
});//Meteor.call
});
My problems are marked by the comments //HERE
Meteor.connection._lastSessionId returns undefined at onCreated event. However if I try to get on click event it works just fine. Why is this caused, what's a workaround for this?
You're attempting to log the session ID before the connection has received it. For example, wrap your call in a setTimeout:
...
setTimeout(() => {
console.log(Meteor.connection._lastSessionId);
}, 500);
...
You might have to tweak the timeout value a bit, but it will be logged. Using setTimeout in this fashion really isn't that reliable though, as the amount of time it takes for the session ID to get set can vary. You'll likely want to look into setting up some kind of simple polling to continuously check for the session ID, until it's set.
Basically _lastSessionId isn't yet available on the client when the template is originally created (it's probably the first template rendered in your app). However there is no need to get this on the client since you're calling a server method anyway, just use the variable directly there where it will already exist!
So simplify:
Meteor.call("updateHistory", {clientIp,clientConnId,visitedOne}, callback)
to:
Meteor.call("updateHistory", visitedOne, callback)
and get the clientIp (if necessary) and use this.connection.id on the server.
This question already has answers here:
How do I return the response from an asynchronous call?
(41 answers)
Closed 6 years ago.
There is a simple field to input a youtube ID. I am using renaldo's youtube api (https://atmospherejs.com/renaldo/youtube-api) to retrieve the title of the youtube clip.
The client side event passes the track ID (var tid) value to the method 'addTrack' successfully - it outputs the track's title to the console on the server. I am having a really bad time returning anything at all back to the client.
If I call the 'getVideoData' method from the 'addTrack' method, it still returns 'undefined' every time. I am no expert in meteor or javascript, this is just something I am learning for the hell of it.
I understand the concept of callbacks and the synchronous nature of javascript (I think!).
Thanks.
[EDIT The code below solves my problem, using Future]
if (Meteor.isServer) {
YoutubeApi.authenticate({
type: 'key',
key: API_KEY
});
Meteor.methods({
addTrack: function(tid) {
Meteor.call("getVideoData", tid, function(err,res) {
console.log(res);
});
},
getVideoData: function(tid) {
var future = new Future();
YoutubeApi.videos.list({
part: "snippet",
id: tid,
}, function (err,data) {
var _data = {"title":data.items[0].snippet.title,"desc":data.items[0].snippet.description};
future["return"](_data)
});
return future.wait();
}
Meteor.startup(function () {
Future = Npm.require('fibers/future');
});
}
Meteor methods are tricky, as they use Fibers to be synchronous (well, they appear to the developer as synchronous). So you need to use Meteor.wrapAsync() to wrap the all to YoutubeApi. I haven't tested the following code, but it should look something like:
Meteor.methods({
getVideoData: function(tid) {
var syncYT = Meteor.wrapAsync(YoutubeApi.videos.list);
var data = syncYT({part: "snippet",id: tid,});
var transformed = {"title":data.items[0].snippet.title,"desc":data.items[0].snippet.description};
console.log(transformed.title);
return transformed.title;
}
});
You'll want to read more about error handling in this case, but this should get you going. Just remember that client-side Meteor is always asynchronous and server-side is optionally asynchronous. Use wrapAsync or Futures to handle async needs in methods.
Lastly, the Meteor guide is great, use it!
I wasn't able to find any information on how you can sleep for 1 second or limit the amount of calls per some time in meteor or javascript seems like there is no simple sleep function? Here is my code how should I go about limiting or sleeping the right way? (Or should I just hack my way on doing this?) all tips are welcome!
call = function(method, endpoint, params) {
try {
params = _.extend({}, params || {});
var data = HTTP.call(method, endpoint, {data: params});
return data;
} catch (err) {
throw new Error("Failed to fetch call " + err.message);
}
};
OK, So supposedly there is undocumented server only sleep function in Meteor Meteor._sleepForMs(5000); simpler than using setTimeout function. I didn't choose what I will pick but this might help somebody else as well.
Good way to make pauses or intervals in Meteor is to use Timers, such as setTimeout()
or setInterval()
When trying to render my template, i want to load the data from the server. I'm trying to use Meteor.call but as per the documentation, i'm clearly not in a stub.
If I use Meteor.call inside of an event handler, the response i get back is correct. If i call it within the template.created or similar, i get an undefined response. I guess i could use async call to do it and then render it when available. But is there another way?
I don't want the clients to have direct access to the DB, i want it to come from the server.
//This doesn't work
Template.config.created = function() {
console.log(Meteor.call('getValue')); //returns undefined
};
//This works
Template.config.events({
'blur #button' : function () {
console.log(Meteor.call('getValue')); //Prints value
}
Any clues?
D
You need to use a callback in your Meteor.call
Template.config.created = function() {
Meteor.call('getValue', function(error, data) {
if(error){
//do stuff to handle error
}
console.log(data);
});
};
From the docs:
On the client, if you do not pass a callback and you are not inside a stub, call will return undefined, and you will have no way to get the return value of the method. That is because the client doesn't have fibers, so there is not actually any way it can block on the remote execution of a method.
I'm not sure why your event handler call is working... There isn't any way to synchronously get a server response like that in JavaScript without Fibers. The solution is simply to provide an asynchronous callback. This isn't really a Meteor limitation, it's just a JavaScript limitation.