One of the challenges with Apps Script V8 compared to Rhino is that you can’t be sure of the order of global variable initialization. Personally, this is not an issue for me, as I avoid any such use of global space, but it is an issue for some. A nice way of dealing with global space is to use namespaces and IEF as I described in Apps Script V8: Multiple script files, classes and namespaces but another, perhaps less obvious way, is put all the variables (and even functions) you need to access from multiple functions in a Keystore. This comes with a few other goodies that I’ll go through in this article.

Redux, Vuex, Mobx etc

Managing user state in client-side apps can be a mess, which is why many developers use some of these ‘state containers’ to hold what is essentially volatile global data. We don’t really need that in apps script, but a Keystore can do a similar job in a basic way.

The KeyStore class

We’ll start with this very simple class, the code for it is at the end of this article

Any variables that you would consider putting in global (for example settings, user states) and so on are good candidates for a key store instead. But first, let’s look at how it works

check for existence

  const keyStore = new KeyStore()
  // check for existence
  // false
  console.log (keyStore.hasStore('prop'))

add something

  // add something
  keyStore.setStore('firstName', 'john')
  // 'john'
  console.log (keyStore.getStore('firstName'))

add an object

  // add an object
  keyStore.setStore('name', {
    firstName: 'john',
    lastName: 'doe',
    id: 1
  })
  console.log(keyStore.getStore('name'))

By reference

objects are by reference, so don’t need to be reset if modified

  // it's not immutable
  keyStore.getStore('name').id = 2
  // {firstName: 'john',lastName: 'doe',id: 2}
  console.log(keyStore.getStore('name'))

Default value

You can set a default value to use if the item is not already in the store

  // set a default value
  keyStore.getStore('names', []).push({
    firstName: 'john',
    lastName: 'doe',
    id: 1
  })
  // [{ firstName: 'john', lastName: 'doe', id: 1}]
  console.log(keyStore.getStore('names'))

Default function

It’s better to use a function for this, so it only gets executed the first time you reference the key

  keyStore.getStore('names', () => []).push({
    firstName: 'jane',
    lastName: 'doe',
    id: 2
  })
  // [{ firstName: 'john', lastName: 'doe', id: 1}]
  console.log(keyStore.getStore('names'))

Key passed to default function

Sometimes it’s handy to have the key in the default value constructor

  // the key is passed to the init function
  keyStore.getStore('doe', key=>({
    lastName: key,
    members:[]
  })).members.push({
    firstName: 'jane'
  })
  // { lastName: 'doe', members:[{firstName: 'jane' }]
  console.log(keyStore.getStore('doe'))

The global space problem

Here’s how to apply it to solving the global space problem.

A wrapper

I generally create an IEF wrapper for each store I want to create – you may want more than one.

// use an IEF wrapper to ensure executed first
const store =(()=>({
  init: function () {
    this.keyStore = new KeyStore()
    return this
  }
}))()

So far then, this would only put the variable ‘store’ in global space, but it would be defined before anything gets executed, so, therefore, would be visible in any function in any file.

Initialization

In your main script, say App.gs

const app = () => {
  // do this once
  store.init()
  ... other stuff
}

Now you can share data between any functions in any files

const fa = () => {
  store.keyStore.getStore('doe', key=>({
    lastName: key,
    members:[]
  })).members.push({
    firstName: 'jane'
  })
}
const fb = () => {
  const doe = store.keyStore.getStore('doe')
  console.log(doe)
  doe.members.push({firstName: 'john'})
}
const fc = () => {
  console.log(store.keyStore.getStore('doe'))
}

and App.gs

const app = () => {
  // do this once
  store.init()

  fa()
  fb()
  fc()
}

Keystore class code

/**
 * this is  a handy store, 
 * esp for dealing with things that might otherwise need to be in global like user states
 */
class KeyStore {

  constructor () {
    this.clear()
  }

  clear () {
    this._storeMap = new Map()
    this._startedAt = new Date().getTime()
  }

  /**
   * 
   * @param {*} key a map key
   * @param {*} [seed] an optional value to set if there's no existing key - it can also be a function
   * @return {*} the value
   */
  getStore (key,seed)  {
    if (seed === null && !this.hasStore(key))throw 'store key not found ' + key
    return this.hasStore(key) 
      ? this.storeMap.get(key) 
      : (this.setStore(key,typeof seed === 'function' ? seed(key) : seed))
  }

  /**
   * check if a key exists
   * @param {*} key 
   * @return {*} the value 
   */
  hasStore (key) {
    return this.storeMap.has(key)
  }

  /**
   * (over) write a value to store
   * @param {*} key 
   * @return {*} the value 
   */
  setStore  (key, value) {
    this.storeMap.set(key,value)
    return this.getStore(key)
  }

  /**
   * get an array of the all the keys in the store
   * @return {*[]}
   */
  get keys () {
    return Array.from(this.storeMap.keys())
  }

  /**
   * get how many items in the store
   */
  get size () {
    return this.storeMap.size
  }

  /**
   * this is the map to hold all the entries
   */
  get storeMap () {
    return this._storeMap
  }
}

Summary

This approach lends itself very well to dealing with the global space problem, but it also abstracts away where the data comes from. One store could be using the KeyStore in memory class, but another could use say, the property store for more permanent storage, or cache or even a database, yet the mechanism for accessing it would remain exactly the same. You could even build a small rudimentary NoSQL database easily using this approach.  Key abstraction is something I’ll cover in a later post.