Saturday, June 18, 2016

Client Side Caching for jQuery

Updates: 6/26/16

  • Fixed bug where triple equals null check would miss.
  • Added support for data driven cache key.
  • Removed let and const statements (some minifiers were having a hard time with them)

Original:

There is great question on Stack Overflow about caching a jquery ajax response in javascript/browser. Unfortunately, even thought it was a good solution, it did not do quite what I needed it to.

The application I was trying to optimize sometimes made redundant parallel requests, and I needed my caching solution to include a queuing system to prevent duplicate fetches.

Below is a simple solution that uses jQuery.ajaxPrefilter to check a local cache prior to making GET requests. Additionally, it will queue the callback if the request is already in flight. The cache stores the queue in both memory and local storage, ensuring that the cache will persist across page loads.

Implementation

(function ($) {
  "use strict";
 
  var timeout = 60000;
  var cache = {};
 
  $.ajaxPrefilter(onPrefilter);
 
  function onPrefilter(options, originalOptions) {
    if (options.cache !== true) {
      return;
    }
 
    var callback = originalOptions.complete || $.noop;
    var cacheKey = getCacheKey(originalOptions);
 
    options.cache = false;
    options.beforeSend = onBeforeSend;
    options.complete = onComplete;
 
    function onBeforeSend() {
      var cachedItem = tryGet(cacheKey);
 
      if (!!cachedItem) {
        if (cachedItem.data === null) {
          cachedItem.queue.push(callback);
        } else {
          setTimeout(onCacheHit, 0);
        }
 
        return false;
      }
 
      cachedItem = createCachedItem();
      cachedItem.queue.push(callback);
      setCache(cacheKey, cachedItem);
      return true;
 
      function onCacheHit() {
        invoke(callback, cachedItem);
      }
    }
 
    function onComplete(data, textStatus) {
      var cachedItem = tryGet(cacheKey);
 
      if (!!cachedItem) {
        cachedItem.data = data;
        cachedItem.status = textStatus;
        setCache(cacheKey, cachedItem);
 
        var queuedCallback;
        while (!!(queuedCallback = cachedItem.queue.pop())) {
          invoke(queuedCallback, cachedItem);
        }
 
        return;
      }
 
      cachedItem = createCachedItem(data, textStatus);
      setCache(cacheKey, cachedItem);
      invoke(callback, cachedItem);
    }
  }
 
  function tryGet(cacheKey) {
    var cachedItem = cache[cacheKey];
 
    if (!!cachedItem) {
      var diff = new Date().getTime() - cachedItem.created;
 
      if (diff < timeout) {
        return cachedItem;
      }
    }
 
    var item = localStorage.getItem(cacheKey);
 
    if (!!item) {
      cachedItem = JSON.parse(item);
 
      var diff = new Date().getTime() - cachedItem.created;
 
      if (diff < timeout) {
        return cachedItem;
      }
 
      localStorage.removeItem(cacheKey);
    }
 
    return null;
  }
 
  function setCache(cacheKey, cachedItem) {
    cache[cacheKey] = cachedItem;
 
    var clone = createCachedItem(cachedItem.data, cachedItem.status, cachedItem.created);
    var json = JSON.stringify(clone);
    localStorage.setItem(cacheKey, json);
  }
 
  function createCachedItem(data, status, created) {
    return {
      data: data || null,
      status: status,
      created: created || new Date().getTime(),
      queue: []
    };
  }
 
  function invoke(callback, cachedItem) {
    if ($.isFunction(callback)) {
      callback(cachedItem.data, cachedItem.status);
    }
  }
  
  function getCacheKey(originalOptions) {
    if (!!originalOptions.data) {
      return originalOptions.url + "?" + JSON.stringify(originalOptions.data);
    }
 
    return originalOptions.url;
  }
 
})(jQuery);

Enjoy,
Tom

No comments:

Post a Comment

Real Time Web Analytics