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.