Skip to main content

Ajax Cache backed by localStorage for jQuery

/**
 * https://github.com/SaneMethod/jquery-ajax-localstorage-cache
 */
; (function($, window){
    'use strict';

    /**
     * Generate the cache key under which to store the local data - either the cache key supplied,
     * or one generated from the url, the type and, if present, the data.
     */
    var genCacheKey = function(options) {
        var url;

        // If cacheKey is specified, and a function, return the result of calling that function
        // as the cacheKey. Otherwise, just return the specified cacheKey as-is.
        if (options.cacheKey){
            return (typeof options.cacheKey === 'function') ?
                options.cacheKey(options) : options.cacheKey;
        }

        url = options.url.replace(/jQuery.*/, '');

        // Strip _={timestamp}, if cache is set to false
        if (options.cache === false) {
            url = url.replace(/([?&])_=[^&]*/, '');
        }

        return url + options.type + (options.data || '');
    };

    /**
     * Determine whether we're using localStorage or, if the user has specified something other than a boolean
     * value for options.localCache, whether the value appears to satisfy the plugin's requirements.
     * Otherwise, throw a new TypeError indicating what type of value we expect.
     * @param {boolean|object} storage
     * @returns {boolean|object}
     */
    var getStorage = function(storage){
        if (!storage) return false;
        if (storage === true) return window.localStorage;
        if (typeof storage === "object" && 'getItem' in storage &&
            'removeItem' in storage && 'setItem' in storage)
        {
            return storage;
        }
        throw new TypeError("localCache must either be a boolean value, " +
            "or an object which implements the Storage interface.");
    };

    /**
     * Remove the item specified by cacheKey and its attendant meta items from storage.
     * @param {Storage|object} storage
     * @param {string} cacheKey
     */
    var removeFromStorage = function(storage, cacheKey){
        storage.removeItem(cacheKey);
        storage.removeItem(cacheKey + 'cachettl');
        storage.removeItem(cacheKey + 'dataType');
    };

    /**
     * Prefilter for caching ajax calls.
     * See also $.ajaxTransport for the elements that make this compatible with jQuery Deferred.
     * New parameters available on the ajax call:
     * localCache   : true // required - either a boolean (in which case localStorage is used), or an object
     * implementing the Storage interface, in which case that object is used instead.
     * cacheTTL     : 5,           // optional - cache time in hours, default is 5.
     * cacheKey     : 'post',      // optional - key under which cached string will be stored.
     * isCacheValid : function  // optional - return true for valid, false for invalid.
     * isResponseValid: function // optional - return true to cache response, false to skip caching response.
     * thenResponse: function // optional - chains on request to potentially alter the response data that
     * gets stored - must return whatever you want stored.
     * @method $.ajaxPrefilter
     * @param options {Object} Options for the ajax call, modified with ajax standard settings.
     * @param orginalOptions {object} Options for ajax as specified in the original call.
     * @param jqXHR {jQuery.xhr} jQuery ajax object.
     */
    $.ajaxPrefilter(function(options, originalOptions, jqXHR){
        var storage = getStorage(options.localCache),
            hourstl = options.cacheTTL || 5,
            cacheKey = options.cacheKey = genCacheKey(options),
            cacheValid = options.isCacheValid,
            responseValid = options.isResponseValid,
            thenResponse = options.thenResponse || null,
            ttl,
            value;

        if (!storage) return;
        ttl = storage.getItem(cacheKey + 'cachettl');

        if (cacheValid && typeof cacheValid === 'function' && !cacheValid()){
            removeFromStorage(storage, cacheKey);
            ttl = 0;
        }

        if (ttl && ttl < +new Date()){
            removeFromStorage(storage, cacheKey);
            ttl = 0;
        }

        value = storage.getItem(cacheKey);
        if (!value){
            // If value not in the cache, add a then block to request to store the results on success.
            jqXHR.then(thenResponse).then(function(data, status, jqXHR){
                var strdata = data,
                    dataType = options.dataType || jqXHR.getResponseHeader('Content-Type') || 'text/plain';

                if (!(responseValid && typeof responseValid === 'function' && !responseValid(data, status, jqXHR))) {

                    if (dataType.toLowerCase().indexOf('json') !== -1) strdata = JSON.stringify(data);

                    // Save the data to storage catching exceptions (possibly QUOTA_EXCEEDED_ERR)
                    try {
                        storage.setItem(cacheKey, strdata);
                        // Store timestamp and dataType
                        storage.setItem(cacheKey + 'cachettl', +new Date() + 1000 * 60 * 60 * hourstl);
                        storage.setItem(cacheKey + 'dataType', dataType);
                    } catch (e) {
                        // Remove any incomplete data that may have been saved before the exception was caught
                        removeFromStorage(storage, cacheKey);
                        console.log('Cache Error:'+e, cacheKey, strdata);
                    }
                }
            });
        }
    });

    /**
     * This function performs the fetch from cache portion of the functionality needed to cache ajax
     * calls and still fulfill the jqXHR Deferred Promise interface.
     * See also $.ajaxPrefilter
     * @method $.ajaxTransport
     * @params options {Object} Options for the ajax call, modified with ajax standard settings and our
     * cacheKey for this call as determined in prefilter.
     */
    $.ajaxTransport("+*", function(options){
        if (options.localCache)
        {
            var cacheKey = options.cacheKey,
                storage = getStorage(options.localCache),
                dataType = options.dataType || storage.getItem(cacheKey + 'dataType') || 'text',
                value = (storage) ? storage.getItem(cacheKey) : false;

            if (value){
                // In the cache? Get it, parse it to json if the dataType is JSON,
                // and call the completeCallback with the fetched value.
                if (dataType.toLowerCase().indexOf('json') !== -1) value = JSON.parse(value);
                return {
                    send: function(headers, completeCallback) {
                        var response = {};
                        response[dataType] = value;
                        completeCallback(200, 'success', response, '');
                    },
                    abort: function() {
                        console.log("Aborted ajax transport for json cache.");
                    }
                };
            }
        }
    });
})(jQuery, window);