I’ve been working on a CardService Add-on lately which also uses HtmlService, and also runs quite a few things Server side. All of these modes need to share state, and it always ends up rather messy. I though I’d take some inspiration from Redux and Vuex and apply those techniques to centralize and simplify state management from the business going on in each of these separate but co-operating environments
This Add-on is actually a Sheets Add-on. It takes documents (images, pdfs etc) and runs them through the DocumentAI API, using ML to decipher invoices and other kinds of material and load the results into a sheet. It’s neither finished nor published yet, but I’m going to take this opportunity to get my thoughts about state management down while I’m still working on it.
Here’s an approximation of what the Add-on looks like in operation at this point.
State management complexities
Its always been a little challenging sharing state cleanly between HTMLService based Add-ons and Server side functions, but the added dimension of Cardservice needs some special attention. When the contents of a card changes you need to recreate the card (as opposed to just a component of it), so all your state is going to be messed up. People usually use Property service to manage this, but Property service calls peppered around the card service code is error prone and mind bending.
In this Add-on, I also needed a filePicker, and CardService doesn’t have one. Luckily I had a pre-baked one from another Add-on (slidesmerge) so I just needed to resurrect that and fire it off as a webapp from a CardService component. However, how to cleanly get the results from the filepicker back over to the re-rendered card service without adding to complexity?
State management needs
The types of state I need to manage and share are
- Add-on global settings – Script property service to persist across sessions for all users
- Add-on user settings – User property service to persist across sessions for each user
- CardService component values – User propety service to persist across sessions for each user
- Results from HTML service – User Cache service to persist for a little while, and usually provoked as the result of changes to Card Service components
- Large scale results from Server Side API calls – User Cache to persist for a little while, and usually provoked from the combination of HTML service results and CardService component values
- Archived results – Cloud Storage – persist forever to avoid duplicate processing of the same file with the same parameters that woudl give the same result as a previous run
Another 2 challenges specific to this API, is that calling the DocumentAI is neither free, nor especially cheap – so I wanted to ensure that if any call was made to the documentAI it was truly for a new document I’d never seen before – so it’s more cost effective (and efficient) to store all results to Cloud storage, indexed by the md5Hash of a file along with its processing parameters, in addition to the short term version held in cache. (as an aside, these results can be quite hefty – so I’m using Apps script caching with compression and enhanced size limitations to write any size of data to Apps Script cache). It’s also used in SuperFetch caching: How does it work?
Another requirement is to make debugging these environments a little easier. It occurred to me that if i centralized all state management and wrapped each call to these various stores not only could I write a script to examine exactly what was in store at any given time, even when the Add-on was actually active, but i could also log and keep a record of state changes.
State management solution
First of all, a simple Store Class to wrap property store gets and sets
Next an AppStore, where all state is handled
Finally, a client side Provoker to get and set state all the state values from within html service. This is an enhancement of the technique i use in How to use Vue.js, Vuex and Vuetify to create Google Apps Script Add-ons
Using the AppStore
Now I don’t need to worry about the details of how state is maintained.
For example, to see what files have been selected by the latest pick action, I can just run this – even while the Add-on is active
In the card service, I can do this kind of thing –
Getting a currently selected value
Setting the result of an api call
Mixing settings, tokens and component values
Dealing with a value change in a component
Displaying results from file picker
In the client side html service code
Setting file picker results
Getting filePicker parameters
Server side functions
This approach reduces the complexity of the main code and centralizes the state logic in one place, irrespective of whether you are using it server side, client side or in the card service avoiding all that timewasting of accidental mistreatments of state values and their side effects.
It also means you can easily start off where you left off – so useful not only for Add-ons but also for spltting up runs that migh otherwise go over execution time.
The best bit though is the ability to log all state changes with one setting and to be able to run tests to inspect state values while the Add-on is active, or even after it’s finished and exited.
I’ll publish the full code of this Add-on in the future. Ping me if you’d like to know morein the meantime.