smt
10/31/2014 - 9:08 PM

Stache

Stache

<!doctype html>
<html>

<head>
    <title>Stache Example</title>
    <script src="https://gist.githack.com/smt/f3e86363de4ab89fc5ec/raw/e722954f8442c71e1c14bead3d2d45992035d323/stache.js"></script>
</head>

<body>
<script>
    var stache = new Stache({ttl:2000, longTtl:4000, debug:true})

    setTimeout(function () {
        console.log('--- Getting SESSION STORAGE initial poop')
        stache.get('poop') }, 1000)
    setTimeout(function () {
        console.log('--- Setting SESSION STORAGE poop to fart')
        stache.set('poop', 'fart') }, 1100)
    setTimeout(function () {
        console.log('--- Getting SESSION STORAGE poop within cache expire time')
        stache.get('poop') }, 2000)
    setTimeout(function () {
        console.log('--- Getting SESSION STORAGE poop after cache expire time')
        stache.get('poop') }, 3200)
    setTimeout(function () { 
        console.log('--- Setting SESSION STORAGE poop to null')
        stache.set('poop', null) }, 3900)
    setTimeout(function () { 
        console.log('--- Getting SESSION STORAGE poop within cache expire time')
        stache.get('poop') }, 4000)
    setTimeout(function () { 
        console.log('--- Getting SESSION STORAGE poop after cache expire time')
        stache.get('poop') }, 6000)

    setTimeout(function () { 
        console.log('--- Getting LOCAL STORAGE initial foo')
        stache.get('foo') }, 7000)
    setTimeout(function () { 
        console.log('--- Setting LOCAL STORAGE foo to bar')
        stache.set('foo', 'bar', true) }, 7100)
    setTimeout(function () { 
        console.log('--- Getting LOCAL STORAGE foo within cache expire time')
        stache.get('foo') }, 8000)
    setTimeout(function () { 
        console.log('--- Getting LOCAL STORAGE foo after cache expire time')
        stache.get('foo') }, 11200)
    setTimeout(function () { 
        console.log('--- Setting LOCAL STORAGE foo to null')
        stache.set('foo', null, true) }, 11900)
    setTimeout(function () { 
        console.log('--- Getting LOCAL STORAGE foo within cache expire time')
        stache.get('foo') }, 13000)
    setTimeout(function () { 
        console.log('--- Getting LOCAL STORAGE foo after cache expire time')
        stache.get('foo') }, 16000)
</script>
</body>

</html>
(function (window, document, undefined) {
    "use strict";

    var _cache;
    var _local = window.localStorage;
    var _session = window.sessionStorage;

    /**
     * Validate a well-formed cache/storage object (used internally).
     * @function
     * @param {Object} item
     * @returns {Boolean}
     */
    var validateItem = function validateItem(item) {
        return (item &&
                item.hasOwnProperty('id') &&
                item.hasOwnProperty('value') &&
                item.hasOwnProperty('expires') &&
                item.hasOwnProperty('isLongTerm') &&
         typeof item.id         !== 'undefined' &&
         typeof item.value      !== 'undefined' &&
         typeof item.expires    !== 'undefined' &&
         typeof item.isLongTerm !== 'undefined')
    };

    /**
     * Creates a new Stache object
     * @class Stache
     * @param {Object} [options]
     */
    var Stache = function Stache(options) {
        var _options = options;
        if (!_options) {
            _options = {};
        }

        // Ensure this was called with the 'new' keyword
        if (!(this instanceof Stache)) {
            return new Stache(options);
        }

        if (_cache) {
            console.warn('There can only be one Stache cache');
            return this;
        }

        _cache = {};

        /** @member {Number} */
        this.ttl = _options.ttl || 1000*60*60*24;

        /** @member {Number} */
        this.longTtl = _options.longTtl || 1000*60*60*24*30;

        /** @member {String} */
        this.ns = _options.ns || 'Stache';

        if (_options.debug) {
            console.log('Cleaning localStorage and sessionStorage (for testing only)');
            _local.removeItem(this.ns);
            _session.removeItem(this.ns);
        }
    };

    /**
     * Add a TTL value to a date and return a new date to use as expiry value.
     * @private
     * @param {Number} [ttl]
     * @param {Date} [now]
     * @returns {Date}
     */
    Stache.prototype._calculateExpiresDate = function _getNewExpiresDate(ttl, now) {
        var _now = (typeof now === 'object' && now.toDateString) ? now : new Date();
        var _ttl = ttl || this.ttl;
        return new Date(_now.valueOf() + _ttl);
    };

    /**
     * Create a new well-formed cache/storage object (used internally).
     * @private
     * @param {String} key
     * @param {*} value
     * @param {Boolean} [isLongTerm]
     * @returns {Object}
     */
    Stache.prototype._createNewItem = function _createNewItem(key, value, isLongTerm) {
        var _value = (typeof value === 'undefined') ? null : value;
        var _isLongTerm = !!isLongTerm;
        return {
            id: key,
            value: _value,
            expires: this._calculateExpiresDate((isLongTerm ? this.ttl : this.longTtl)),
            isLongTerm: _isLongTerm
        };
    };

    /**
     * @private
     * @param {String} key
     * @returns {Object}
     */
    Stache.prototype._getCacheItem = function _getCacheItem(key) {
        if (!key) {
            console.warn('Please provide a key');
            return;
        }

        var item = _cache[key];
        return validateItem(item) ? item : null;
    };

    /**
     * @private
     * @param {Object} item
     */
    Stache.prototype._setCacheItem = function _setCacheItem(item) {
        if (!validateItem(item)) {
            console.warn('Item was not set or properly formed: ', item);
            return;
        }

        _cache[item.id] = item;
    };

    /**
     * @private
     * @param {String} key
     */
    Stache.prototype._deleteCacheItem = function _deleteCacheItem(key) {
        if (!key) {
            console.warn('Please provide a key');
            return;
        }

        if (typeof _cache[key] !== 'undefined') {
            _cache[key] = null;
            delete _cache[key];
        }
    };

    /**
     * @private
     * @param {String} key
     * @returns {Object}
     */
    Stache.prototype._getStoreItem = function _getStoreItem(key) {
        if (!key) {
            console.warn('Please provide a key');
            return;
        }

        var item = _cache[key];

        // default to session storage
        var storeMechanism = (validateItem(item) && item.isLongTerm) ? _local : _session;
        var storeObj = JSON.parse(storeMechanism.getItem(this.ns));

        if (!storeObj) {
            return null;
        }

        item = (typeof storeObj[key] === 'undefined') ? null : storeObj[key];

        return item;
    };

    /**
     * @private
     * @param {Object} item
     */
    Stache.prototype._setStoreItem = function _setStoreItem(item) {
        if (!validateItem(item)) {
            console.warn('Item was not set or properly formed: ', item);
            return;
        }

        var storeMechanism = item.isLongTerm ? _local : _session;
        var storeObj = JSON.parse(storeMechanism.getItem(this.ns));

        if (!storeObj) {
            storeObj = {};
        }
        storeObj[item.id] = item;

        storeMechanism.setItem(this.ns, JSON.stringify(storeObj));
    };

    /**
     * @private
     * @param {String} key
     */
    Stache.prototype._deleteStoreItem = function _deleteStoreItem(key) {
        if (!key) {
            console.warn('Please provide a key');
            return;
        }

        var item = _cache[key];

        if (!validateItem(item)) {
            console.warn('Item was not set or properly formed: ', item);
            return;
        }

        var storeMechanism = item.isLongTerm ? _local : _session;
        var storeObj = JSON.parse(storeMechanism.getItem(this.ns));

        storeObj[item.id] = null;
        delete storeObj[item.id];

        storeMechanism.setItem(this.ns, JSON.stringify(storeObj));
    };

    /**
     * @private
     * @param {Object} item
     * @returns {Boolean}
     */
    Stache.prototype._isExpired = function _isExpired(item) {
        if (!item) {
            console.warn('Please provide an item');
            return;
        }

        var now = new Date();

        return new Date(item.expires.valueOf()) < now;
    };

    /**
     * @method
     * @param {String} key
     * @returns {*}
     */
    Stache.prototype.get = function get(key) {
        var now = new Date();
        var storeObj;
        var item = this._getCacheItem(key);

        if (!item) {

            // prime cache
            storeObj = this._getStoreItem(key);
            if (!storeObj) {
                storeObj = this._createNewItem(key, null);
            }
            this._setCacheItem(storeObj);
            item = this._getCacheItem(key);

            console.log('Initialized new cache item because blank "' + key + '"');
            console.table(_cache);

        } else if (this._isExpired(item)) {

            // refresh expired cached item from storage
            storeObj = this._getStoreItem(key);
            if (validateItem(storeObj)) {
                item.value = storeObj.value;
            }
            item.expires = this._calculateExpiresDate((item.isLongTerm ? this.ttl : this.longTtl));
            this._setStoreItem(item);
            this._setCacheItem(item);

            console.log('Updated cache item because expired "' + key + '" (' + item.value + ')');
            console.table(_cache);

        } else {

            console.log('No cache init or update required, our data is still fresh.');
            console.table(_cache);

        }

        console.log('It is now ' + now.toString() + ' and _cache[' + key + '] expires at ', item.expires.toString());

        return item.value;
    };

    /**
     * @method
     * @param {String} key
     * @param {*} value
     * @param {Boolean} [isLongTerm]
     */
    Stache.prototype.set = function set(key, value, isLongTerm) {
        var now = new Date();
        var item = this._getCacheItem(key);

        if (!item) {

            item = this._createNewItem(key, value, isLongTerm);
            console.log('Initialized new cache item because blank "' + key + '"');
            console.table(_cache);

        } else {

            item.value = value;
            item.expires = this._calculateExpiresDate((isLongTerm ? this.ttl : this.longTtl));
            console.log('Updated existing cache item "' + key + '" (' + value + ') : ' + now.toString());
            console.table(_cache);

        }

        // Ensure we never save the same key to two different types of storage.
        // If a different isLongTerm option is passed, delete the existing storage item.
        if (item.isLongTerm !== !!isLongTerm) {
            this._deleteStoreItem(key);
            item.isLongTerm = isLongTerm;
        }

        this._setStoreItem(item);
        this._setCacheItem(item);
    };

    /**
     * @method
     * @param {String} key
     * @param {Boolean} [confirmDeleteAll]
     */
    Stache.prototype.del = function del(key, confirmDeleteAll) {
        // Provide the means to wipe the whole slate clean if desired.
        if (!key && confirmDeleteAll) {
            _session.removeItem(this.ns);
            _local.removeItem(this.ns);
            _cache = null;
            _cache = {};
            console.log('Nuked the Stache, kaboom!');
            return;
        }

        // Important to delete store item first so that the storage mechanism, if any, can be determined
        this._deleteStoreItem(key)
        this._deleteCacheItem(key)
    };

    return window.Stache = Stache;

}(this, this.document));