Skip to content
This repository has been archived by the owner on Mar 15, 2018. It is now read-only.

Guide to Caching

Matt Basta edited this page Aug 26, 2013 · 3 revisions

Building a responsive and interactive site requires a large amounts of caching and cache manipulation techniques to create a usable product. Without caching, site responsiveness would be poor, and without cache manipulation, the behavior of the site would appear buggy and broken.

Caching

There are two types of caches in Commonplace. The first is called the general cache or the request cache. The general cache is a basic key-value store where keys are strings and values are any data type. The general cache is used for, as its name implies, general purpose caching of processed or unprocessed blobs of information.

The second type of cache is called the model cache. The model is a more flexible data store, which organizes objects in a key-value way where the key is a predefined type (generally strings, but could be numbers) and the value is a JavaScript object. The model cache can store many types (or "models") of data and is used to store discrete objects rather than lists or blobs.

General Caching

Interface

The general cache is managed in the cache module. It provides the following interface:

cache.has(key)

Returns a boolean describing whether or not the string key exists in the cache.

cache.get(key)

Returns the value associated with the string key in the cache. If the key is not in the cache, this function returns undefined.

cache.set(key, value)

Sets the value value to the string key as the key in the cache.

If key is matched by a rewriter defined in the rewriters module, the value will be manipulated.

cache.purge([filter])

Removes all entries from the cache. If filter is truthy, it will be called with each cache entry's key as the first argument. If the return value of filter is truthy, the key will be preserved in the cache.

cache.bust(key)

Deletes the entry with the the string key as the key from the cache.

cache.attemptRewrite(matcher, worker[, limit]) (manipulation)

Attempts to rewrite the cache. For each entry in the cache, matcher will be called with the entry's key as the first argument. If matcher returns a truthy value, worker will be called with the entry's value as the first argument and the entry's key as the second argument. The return value of worker will replace the entry's value in the cache.

If limit is truthy, it is expected to be a positive integer. After matcher has returned limit truthy values, attemptRewrite will return.

If a matched entry's key exists in the persistent cache, the persistent cache entry's value will be replaced with the value returned by worker.

cache.raw

A JavaScript object containing the cache.

cache.persist

This object contains a nearly identical interface to the cache module itself, with the following exceptions:

  • There is no cache.persist.persist, cache.persist.raw, or cache.persist.attemptRewrite.
  • Values set with cache.persist.set() will be persisted after the session has ended (using the storage module).
  • Entries created via cache.persist will be saved to the "normal" cache as well. cache.persist.set(x, y) is the same as cache.persist.set(x, y); cache.set(x, y);.
  • Entries modified with non-cache.persist methods will not update the persistent cache, with the exception of cache.attemptRewrite(). I.e.: cache.persist.set("foo", "bar"); cache.set("foo", "zap"); alert(cache.persist.get("foo")); will alert "zap" instead of "bar", but the persistent cache (upon refresh) will still contain "bar".
  • cache.persist.purge() does not accept a filter argument.

Manipulation (General Rewriters)

The rewriters module returns an array, which can contain functions in the following format:

function(new_key, new_value, raw) {
   // ...
}

For each key set with cache.set() and cache.persist.set(), each rewriter is called with the new entry's key as the first argument (new_key), the new entry's value as the second argument (new_value), and a copy of the whole cache as the third argument (raw; equivalent to cache.raw).

If a rewriter method returns undefined, it will have no effect.

If a rewriter method returns null, the entry will not be created in the general cache and no other rewriters will be called. If the entry was created via cache.persist.set(), it will still be created in the persistent cache.

If a rewriter method returns a truthy value, the truthy return value will be used in lieu of the value passed to cache.set(). If a new value is returned, any remaining rewriters will still be called and will be called with the updated value.

Usefulness

The usefulness of cache rewriters is to update existing information in the cache instead of simply adding more information. For instance, if data is returned from an API in a paginated way, it's sometimes more useful to store all results in a single cache entry rather than in multiple entries. To accomplish this, a rewriter could detect when cache.set() is called with page N of a results list and update page 1 instead (and return null to prevent the new page from being saved).

A savvy rewriter method could also be used to detect when an outdated cache entry is being created and reject it, or update the entry's value with new information before it is set.

Built-in Usage

The requests module will store all data it receives from GET requests in the general cache.

Model Cache

The model cache is exposed via the models module. The models module itself returns a function which accepts one argument: the name of the model you wish to modify. The following boilerplate module definition is not uncommon:

define('my_module', ['models'], function(models) {
    var app_model = models('app');
    var foo_model = models('foo');
    // ...
});

The models that are available need to be defined in the JavaScript object model_prototypes in the settings module. This object should look like {"model_name": "key"} where "key" is the name of the key in model objects to do primary lookups by (the lookup method, documented below).

Interface

The interface of each model (returned by models("model_name")) has the following interface:

model.cast(object)

The object object will be indexed and stored in the model cache. The model object automatically looks up the object's primary key (defined in settings.model_prototypes) and stores it accordingly.

model.uncast(object)

If object is an object, it will be treated as an entry in the model cache. Its primary key will be extracted, and the equivalent of model.lookup(object[primary_key]) will be returned.

If object is an array of objects, the function will return the equivalent of object.map(model.uncast) (though the function will not recurse further).

This method exists to update existing caches.

model.get(url, key[, getter])

Returns a promise object that produces the model cache entry for key. If the entry associated with key is not in the cache, a new "getter" will be created to supply the promise with data form a URL (url). By default, requests.get will be used, though an alternative getter can be supplied through the getter argument.

model.purge()

Purges the model cache of all entries.

model.del(key[, by])

Deletes the model cache entry with the primary key key. If by is specified, the entry will be matched by the field by instead of the primary key. For instance, models('person').del('Basta', 'last_name') will delete the first model cache entry where entry.last_name === 'Basta'.

model.lookup(key[, by])

Returns the model cache entry with the primary key key. If by is specified, the entry will be matched by the field by instead of the primary key (a la model.del).

Built-in Usage

The builder uses the model cache in conjunction with the as and key parameters, where as lists a model name and key lists the primary key of the model object. Specifically, the builder uses the .get() method to return either an AJAX request to fetch the object or a promise object which simulates a cached response containing the value stored in the model cache.

The builder also "uncasts" each object it encounters when as and pluck are used together on a cached response. This updates objects in the general cache with freshly updated information from the model cache. Consider the following scenario:

Builder requests API:
- Request 1
  - Object A
  - Object B
  - Object C

The builder pushes each object to the model cache:
- "Object" Model
  - A: Object A
  - B: Object B
  - C: Object C

The builder requests another API which contains updated objects. The new
request is stored in the general cache and the model cache is updated. The
cache will appear as follows:
- Request 2
  - Updated Object A
  - Updated Object B
  - Updated Object C
- Request 1
  - Object A
  - Object B
  - Object C

- "Object" Model
  - A: Updated Object A
  - B: Updated Object B
  - C: Updated Object C

If "page 1" is requested again, the builder will fetch Request 1 from the
general cache. But the general cache's copy of Request 1 contains outdated
versions of the objects.

The builder will, instead of using the old values, pass them to `uncast`, which
will return the updated values. The builder will also update the entry for
Request 1 in the general cache with the new objects to allow the old objects to
be released and garbage collected:

- Request 2
  - Updated Object A
  - Updated Object B
  - Updated Object C
- Request 1
  - Updated Object A
  - Updated Object B
  - Updated Object C