This post is part of a series:
Trakt.TV & Subtitles - A Google Apps Script Project
Overview
GAS's native
CacheService is a very good caching service. It provides several levels of caching (user, script, ...) with very good basics for caching string values. But this is also part of it drawbacks:
- It can store only string values
- The values are limited to 128KB
- Some additional features are lacking, such as additional information on entries (e.g. when an entry was last updated)
With these limitations, and the requirements I had in the project I was working on, I decided to implement an enhancement to this service: EnhancedCacheService
The purpose of the new service was to wrap the existing service and add additional features while preserving the existing features and the flexibility of choosing the cache type. The additional features which are currently implemented:
- Support for native JavaScript types, such as: number, boolean, object
- Support for values larger than 128KB
- Additional information on cache entries - get the date an entry was last updated
In order to instantiate an enhanced cache service, use:
var cache = EnhancedCacheService.wrap(CacheService.getUserCache());
As you can see, this gives you the freedom to choose the type of cache you want to use.
Basics
In order to support the requirements I have decided to store a value descriptor instead of the value itself. This answers two main requirements:
- Ability to get additional information on cache entries
- Support in the simple native data types, such as boolean and number
This is done by creating a value descriptor object containing the following information:
- The value, in its original form (number, string, boolean, null)
- The name of the type of the value (e.g. 'string', 'boolean', etc.)
- The time-to-live that was set for the entry
- The time the entry was set (to be used for last updated)
This value descriptor is being stringified (using
JSON.stringify) and stored as the value of the entry.
The method structure that was chosen to support the various type was (where
<Type> is replaced by the specific type, e.g. "
Boolean"):
cache.put<Type>(String key, Type value, Number ttl) : Void
cache.get<Type>(String key) : Type
In each put & get method the key and value are verified to be of the correct type. The ttl parameter (i.e. time-to-live in seconds) is optional, same as in the native cache service.
Support For JavaScript Objects
Objects are a little bit more complex. Since the objects are stored also as strings, there is a need to allow custom methods for stringifying and parsing the object value (although in most cases JSON's default methods are enough, there are some cases such as the Date object where it is not). For this reason, both getObject and putObject methods take an optional parse/stringify method. If not specified, JSON's methods are used.
So, for example, in order to store and get a simple object:
cache.putObject('p1', { name: 'John', age: 30 });
var p = cache.getObject('p1');
The returned value is an object, stringified and parsed using the default methods.
In order to store an object for which the default methods are not enough, the following can be done (in this example, for the
Date object):
var stringifyDate = function(o) { return '' + o.getTime(); };
cache.putObject('d1', new Date(), undefined, stringifyDate);
var parseDate = function(s) { return new Date(+s); };
var d = cache.getObject('d1', parseDate);
Support For Large Values
In order to support larger values but still use the existing cache service I have decided to split large values between several entries. String values are the main candidates for being too long (number, boolean and null have no chance of reaching the max size). Since objects can also be too large (and for some other reasons), I have decided to store them as strings. This allows me to use the same implementation for all relevant value types.
When storing an entry, the value in the value descriptor is checked - if it is a string too long, if so it needs to be split. In such cases the value is split to smaller parts, each is stored in a separate entry. The keys of the split entries are collected and stored as part of the value descriptor instead of the value itself.
When getting a value, the opposite operation is done. If the value descriptor has keys instead of values, the values are taken and rejoined to the original value. Only then the value is returned (or parsed in case of an object).
The code is available in GitHub:
https://github.com/yinonavraham/GoogleAppsScripts/tree/master/EnhancedCacheService