There are some new problems to do with the accessibility of functions in the global space which depend on a number of factors such as the order of declaration, and whether they are in a namespace as well as changes to the scope of ‘this’  in v8. When you access a server side function from client side in an add-on or indeed any htmlservice, you’ll be using google.script.run, which takes this format.

      google.script.run
    
      .withFailureHandler (function(err) {
        reject (err);
      })
    
      .withSuccessHandler (function(result) {
        resolve (result);
      })
    
      .theFunction (args);

and of course relies on ‘theFunction’ being in the global space and correctly available. It is also unable to execute things in libraries or in namespaces, for that same reason. Previously I’ve deployed workarounds using a provoke function – for example in this Using promises to orchestrate Html service polling  , but these techniques are now unreliable with V8

Problem into opportunity

With every problem comes and opportunity, so in the post we’ll look at a better way to –

  • Promisify google.script.run
  • Introduce whitelisting – limit what can be run from the client side
  • Run anything, whether or not it’s in the global space
  • Abstraction of the how from the what

The KeyStore class

In  Apps Script V8: Keystore for global space  I introduced the keystore class to help overcome some of the global space challenges arriving in V8, and it will play a key role in this new method. You’ll need the class to use on the Server side. Here’s the library reference.

1DJmFiT-OoLX9_bd_hmyI3xY57hyiZGfUW42pq8l6zzRXNHoNxQ_lolyt

The whitelist

The core concept is that you no longer reference function names from the client side. Instead you reference them by a key in the keystore which knows how to execute them and is available globally in your server side project. So first you make a whitelist like this.

/**
 * this should contain a list of all the functions authorized
 * to be run from the client 
 * for V8, it's important that it's a namespace ief to ensure its parsed in the correct order
 * and is available throughout the project on initialization
 */
const WhitelistedActions = (()=> {

 // make a new store so its globally available
 const ks = keystore.newKeyStore()
 
 // whitelist everything that can be run
 ks.setStore('get-answer', (...args)=>getAnswer(...args));
 ks.setStore('show-something',(...args)=> ns.show(...args));
 ks.setStore('get-digest', cUseful.Utils.keyDigest);
 ks.setStore('sort', (x => x.sort((a,b)=>a-b)));
 ks.setStore('get-uuid',Utilities.getUuid);

 return ks
})();

This whitelist is going to allow access to all  these kinds of functions, ranging from Apps Script services through to library functions.

Running from the whitelist

The idea here is that all requests to run things Server side pass through a single function that knows how to access the whitelist.

/**
 * run something from the whitelist
 * will generall be invoked by google.script.run
 * @param {string} name the key of the action to run in the whilelistedActions store
 * @param {...*} args any arguments for the action
 * @return {*} the result
 */
const runWhitelist = (name, ...args) => {
  // get what to run from the store
  const action = WhitelistedActions.getStore(name)
  if(!action) {
    throw new Error(`${name} is not in the list of actions that can be run from the client`)
  }
  return action (...args)
}

Running from Server side

Before we head over to the client side, don’t forget you can use this same methodology to expose any function globally to your project, without having to actually put them in the global space. To show that we can test all these functions server side like this.

const localTest = () => {
  Logger.log(runWhitelist ('get-answer' , 2,3))
  runWhitelist ('show-something' ,'hello')
  Logger.log(runWhitelist ('sort' ,[2,1,5]))
  Logger.log(runWhitelist ('get-digest' ,[2,1,5]))
  Logger.log(runWhitelist ('get-uuid'))
}

Running from Client side

To run one of these functions using google.script.run – it would look like this

      google.script.run
    
      .withFailureHandler (function(err) {
        reject (err);
      })
    
      .withSuccessHandler (function(result) {
        resolve (result);
      })
    
      .runWhitelist (name,...runArgs);

Better, but let’s promisify that, so we can run them like this.

Provoke.run('get-uuid')
 .then(uuid=>console.log(uuid))

or if you prefer

uuid = await Provoke.run('get-uuid')

Provoke namespace

Here’s the code for the provoke namespace

/**
 * @namespace Provoke
 * promise management for async calls
 * this is expecting to find a global function called runWhitelist on the server side
 * anything that is able to be run from the client should have been registered in the whitelisted actions store
 * for example
 
     const WhitelistedActions = (()=> {
    
     // make a new store so its globally available
     const ks = keystore.newKeyStore()
     
     // whitelist everything that can be run
     ks.setStore('get-answer', (...args)=>getAnswer(...args));
     ks.setStore('show-something', (...args)=>ns.show(...args));
    
     return ks
    })();
    
 */

const Provoke =(ns=> {

  /**
  * run something asynchronously
  * @param {string} name the name in whitelistedactions
  * @param {[...]} the args
  * @return {Promise} a promise
  */
  ns.run = function (name, ...runArgs) {
    

    // this will return a promise
    return new Promise(function ( resolve , reject ) {
      
      google.script.run
    
      .withFailureHandler (function(err) {
        reject (err);
      })
    
      .withSuccessHandler (function(result) {
        resolve (result);
      })
    
      .runWhitelist (name,...runArgs); 
    });
    
    
  };
  
  
  return ns;
  
})({});

Summary

This approach is a good way to get over the v8 global space problem, indeed getting everything out of the global space is a good strategy anyway, it abstracts and secures by placing everything runnable in a whitelist, gets rid of those untidy callbacks and allows you to access all manner of services and libraries from the client side. Here’s the steps as a reminder

On the server side
  • include the keystore
  • make a WhitelistedActions namespace
  • include the runWhitelist function in the global space
On the client side
  • use the provoke namespace to call actions on the whitelist