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
I ma using SignalR in the browser. Some request (calling function on the server) are long and I would like to show spinner/loading-bar.
Can I somehow hook for an event when this function is started and when it returns back.
I'm trying to figure out what you mean - I think basically you want some way to hook into the start of a call and the end of a call (to load and unload a spinner)?
I've done this in two different ways - firstly as a one-off (first example), and then more systematically (the second example). Hopefully one of these will be what you need.
$.connection.myHub.server.hubMethod().done(function () {
//called on success
}).fail(function (e) {
//called on failure - I don't recommend reading e
}).always(function() {
//called regardless
spinner.close();
});
spinner.open(); // must be triggerd AFTER call incase exception thrown (due to connection not being up yet)
If you don't like that - perhaps because you call hub methods in hundreds of different sections of codes, then there are other tricks which are a bit more complicated. Lets see:
function SetupSpinnerOnCallToSignalrMethod(hubServer, method, spinnerStartCallback, spinnerEndCallback) {
var prevFunc = hubServer[method];
hubServer[method] = function () {
var ret = prevFunc.apply(this, arguments);
spinnerStartCallback(); // must be triggerd AFTER call incase exception thrown (due to connection not being up yet)
ret.always(function() {
spinnerEndCallback();
});
return ret;
};
}
//then call this for each method
SetupSpinnerOnCallToSignalrMethod($.connection.myHub.server,
"hubMethod",
function() { spinner.open(); },
function() { spinner.close(); }
);
//the server call should then work exactly as before, but the spinner open and close calls are invoked each time.
I understand that Meteor methods let you do a client to server call, but what's the best approach to call another function or method from a Meteor method, i.e. a server to server call.
Right now if I do a regular JS function call it only works if the JS file is in the lib folder. But I need it to be in the server folder.
Here is the code
I have a topics collection which sits in the collection folder and has the following
I have the following which is a collection
Meteor.methods({
topicPost: function(topicAttributes) {
var user = Meteor.user(),
topicWithSameTitle = Topics.findOne({title: topicAttributes.title});
// ensure the user is logged in
if (!user)
throw new Meteor.Error(401, "You need to login to add a new topic");
Meteor.call('checkUser');
}
});
I then have the following method which sits in the server folder
Meteor.methods({
checkUser: function () {
alert('aaaa');
}
});
This works, but it's not a great solution. My method for handling this is to have all of my functions outside the Meteor.methods, and simply relay to the proper functions when necessary.
// Client
Meteor.call('foo');
And:
// Server
Meteor.methods({
foo: function() {
foo();
}
});
foo = function() {
foo = bar;
};
The advantage is that the foo fn can be called from anywhere on the server without a Meteor.call. Meanwhile, Meteor.methods only exposes what is absolutely necessary to the client.
[EDIT] There is some ambiguity as to which 'foo' you're talking about; obviously the server knows you mean the one outside the methods call. But if you're feeling confused, you can always rename one or the other. The advantage to this is that there is minimal refactoring involved.
Just to clarify for readers who don't notice that the OP's code actually contains the answer, you just do
Meteor.call('checkUser');
on the server. Per the meteor docs (https://docs.meteor.com/api/methods.html#Meteor-call), on the server, if you use Meteor.call() without a callback argument, the call runs synchronously and waits for the result. For example, if 'checkUser' was written to provide a userId value, you'd just do
let userId = Meteor.call('checkUser');
On the client, though, you have to provide a callback function as an argument to Meteor.call(), and the userId would be provided asynchronously to your callback function.
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.
I defined a Collection in the model.js like this:
People = new Meteor.Collection("people");
Here's the code in main.js:
function test2(){
console.log(JSON.stringify(People.find().fetch()));
setTimeout(test2,5000)
}
if (Meteor.isServer) {
if(People.find().fetch().length === 0){
var tom = {name:"Tom",age:18};
People.insert(tom);
}
Meteor.startup(function () {
test2();
});
}
Here are the errors I get:
I want to Meteor execute some CRUD on the Collection automatically at set intervals. So I'm using setTimeOut, but it seems to be difficult.
Any idea about what I am doing wrong?
Use Meteor.setTimeout instead:
Meteor.setTimeout(test2,5000)
On the server meteor code is run using fibers to let your write synchronous code, javascript's timeout has to let its callback fire in a fiber too, especially if it contains meteor code.