Last updated at Wed, 25 Oct 2017 14:57:03 GMT

Ajax is a great tool for creating responsive dynamic web pages. Although Ajax updates are usually much faster than page reloads, there is still considerable delay when considering Ajax for pages that require real-time responses (content updating in real time).

This gets even more complicated in fully dynamic user interfaces. Interfaces structured within web pages with URLs that allow for anchor and back button navigation and which require real time updating, in particular, can suffer from repeated AJAX requests. One option here is to implement traversing code that only requests the difference between pages, but this approach is overly complicated and is not considered a best practice.

There is however a better solution – caching AJAX requests. Although we can use a standard caching solution provided by HTTP (yes, Ajax is cached by HTTP), there is a catch: It works for GET requests only (not POST). Furthermore, you cannot control cache expiration on the application side. Worse still – page reloads do not clear the cache in IE: Not even with a force reload Ctrl+F5. IE believes Ajax requests are not related to the loaded page and the reload request is not propagated. This means Ajax requests will not be removed until the cache expires.

We decided to solve this using an in-memory JavaScript cache. Since we did not find any implementation online of such a simple caching mechanism here is our own implementation (github) – it is really simple:

// Create cache object with up to 1000 elements
var cache = new ObjectCache( 1000);

// Let's create request and response objects
var request = {
  command: 'get-user-settings',
  user_id: 12
};
var response = ajax_call( request);

// Save the response in the cache
cache.put( request, response)
// Query the request in the cache
cache.get( request);
// Query the request, must not be older than a minute
cache.get( request, 60*1000);

Implementation details

We keep stored objects in a hash table indexed by requests. JavaScript does not provide hash tables out of the box, but we can use JavaScript’s objects which are dynamic in principle. The only obstacle is that elements in JavaScript’s objects are indexed with a string and thus we must convert requests (objects) into a string to make a proper key.

var storage = {};

Conversion from object to string is tricky. We use JSON object-to-string conversion embedded in the language for this purpose. Although it looks like a hack, it works great:

function make_key(key) {
  return JSON.stringify(key);
}

Together with the stored object, we save a timestamp of the creation time and a timestamp of the time of last access. In the put method, both timestamps are the same:

this.put = function(key, value) {
  // Get the key in string
  var cache_key = make_key(key);
  // Get current timestamp
  var now = +new Date();
  // Save the value, access time, and storage time
  storage[cache_key] = [ value, now, now ];

  // Update cache size
  size += storage[cache_key] ? 0 : 1;
  // Check the cache size -- see later
  check_size();

  return value;
}

The get method tests for expiration date if provided and updates the timestamp of the last access.

this.get = function(key, age) {
  // Get the key in string
  var cache_key = make_key(key);
  // Find the element
  var record = storage[cache_key];
  // Return null if not found
  if (record === undefined)
    return null; // Object not found
  var now = +new Date();
  // Check the age of the object
  if (age && record[2] < now-age)
    return null; // Object is too old
  // Update the access timestamp
  record[1] = now;

  // Return the value
  return record[0];
}

The cache is not cleaned up until it’s capacity is reached. The check_size is called from the put method to ensure we are not exceeding the allowed capacity. In the clean up process we remove half of the oldest elements. We take into account access time instead of creation time.

While we can keep objects in a linked list sorted by access time and simply remove the old elements from the tail, this implementation would be overly complicated for this purpose. Instead we simple sort the cache like so:

function check_size() {
  if (size > capacity) {
    var tosort = [];
    for ( var key in storage)
      tosort.push([ key, storage[key] ]);
      tosort.sort(function(a, b) {
      return a[1][1] - b[1][1];
    });
    // Delete half of the capacity
    for ( var i = 0; i < capacity / 2; i++) {
      delete storage[tosort[i][0]];
    }
    size -= capacity / 2;
  }
}

The code can be enjoyed on github, CC.