/*jshint forin:true, noarg:true, noempty:true, eqeqeq:true, bitwise:true, strict:true, undef:true, unused:true, curly:true, browser:true, jquery:true, indent:4, maxerr:200 */ // locache 0.3.2 // // (c) 2012 Dougal Matthews // locache may be freely distributed under the MIT licence. // // locache is a client side caching framework that stores data // with DOM Storage and proves a memcache inspired API for // setting and retrieving values. (function () { "use strict"; // Initial Setup // -------------------- // Save a reference to the global object, in most cases this is `window`. var root = this; // Object context bnding shim to support older versions of IE. var bind = function (func, thisValue) { return function () { return func.apply(thisValue, arguments); }; }; // Cache class constructor. This is the base “class” for // locache and is used for the global instance plus any of your own // custom caches. // The constructor accepts a properties object, and assigns each value // of the object to the instance. At the moment, this is only really used // to set the 'storage' property - so you can choose a builtin or use // your own storage mechanism. function LocacheCache(options) { if (options && options.storage) { this.storage = options.storage; } // Re-bind the context of the two async methods so `this` is equal to // the instance of locache. This allows them to easily access the // other methods and storage objects. This is a bit of hack, and may // not be the best idea. this.async.set = bind(this.async.set, this); this.async.get = bind(this.async.get, this); } // Current version of locache, this is inserted in the build process. LocacheCache.prototype.VERSION = "0.3.2"; // Boolean value that determines if they browser supports localStorage or // not. This is based on the Modernizr implementation that can be found // in [the Modernizr GitHub repository.](https://github.com/Modernizr/Modernizr/blob/c56fb8b09515f629806ca44742932902ac145302/modernizr.js#L696-731) var supportsLocalStorage = LocacheCache.prototype.supportsLocalStorage = (function () { try { // Create a test value and attempt to set, get and remove the // value. These are the core functionality required by locache var test_val = "___locache___"; root.localStorage.setItem(test_val, test_val); root.localStorage.getItem(test_val); root.localStorage.removeItem(test_val); // If any of the checks fail, an exception will be raised. At // that point we can flag the browser as not supporting // localStorage. return true; } catch (e) { return false; } })(); // Boolean value that determines if they browser supports sessionStorage or // not. This is based on the Modernizr implementation that can be found // in [the Modernizr GitHub repository.](https://github.com/Modernizr/Modernizr/blob/c56fb8b09515f629806ca44742932902ac145302/modernizr.js#L696-731) var supportsSessionStorage = LocacheCache.prototype.supportsSessionStorage = (function () { try { // Create a test value and attempt to set, get and remove the // value. These are the core functionality required by locache var test_val = "___locache___"; root.sessionStorage.setItem(test_val, test_val); root.sessionStorage.getItem(test_val); root.sessionStorage.removeItem(test_val); // If any of the checks fail, an exception will be raised. At // that point we can flag the browser as not supporting // sessionStorage. return true; } catch (e) { return false; } })(); // Boolean flag to check if the browser supports native JSON. var supportsNativeJSON = LocacheCache.prototype.supportsNativeJSON = !!root.JSON; // Booleant flat to check if the browser supports HTML postMessage. var supportsPostMessage = LocacheCache.prototype.supportsPostMessage = !!window.postMessage; // Internal utility functions // -------------------- // A defer implempmentation to avoid IO access blocking the current // thread. This is exposed on the LocacheCache prototype, simply so it // can be accessed from within the unit tests. It's not intended for // public use. var defer = LocacheCache.prototype._defer = (function () { // Store of the pending functions var timeouts = []; // Message name used to identify posts related to the defer var messageName = "function-defer-message"; // Add a message event listner. If its not from this window or doesn't // have the message name defined above, don't do anything and allow it // to propogate to other handlers. Otherwise, its meant for us so stop // the event. var addEventListener; if (root.addEventListener) { addEventListener = root.addEventListener; } else if (root.attachEvent) { addEventListener = root.attachEvent; } addEventListener("message", function (event) { if (event.source !== root || event.data !== messageName) { return; } event.stopPropagation(); // Make sure we have some pending functions, otherwise return. if (timeouts.length === 0) { return; } // take the oldest from the 'queue' and call that functions. var fn = timeouts.shift(); fn(); // Return false, to make sure the event isn't propogated return false; }, true); // Constructor for the Defer, takes a function onject and stores it // on itself. function Deferred(fn) { this.fn = fn; } // The defer method runs the function and stores the result. Uppon // finishing, it looks for a finishedFunction, that if exists, is // called and passed the result. Deferred.prototype.defer = function () { this.resultValue = this.fn(); if (this.hasOwnProperty('finishedFunction')) { this.finishedFunction(this.resultValue); } }; // Return if the defer has finished. THis is determined by the // existance of the resultValue on the object. Deferred.prototype.hasFinished = function () { return this.hasOwnProperty('resultValue'); }; // The finished function takes another function and assigns that to // be called when the deferred function has finished. Deferred.prototype.finished = function (fn) { this.finishedFunction = fn; // Check to see if the deferred function has finished, this can // happen if its very quick or the finished function is assigned // late. If it is, call it straight away. if (this.hasFinished()) { this.finishedFunction(this.resultValue); } // Make this object chainable. return this; }; // Wrapper utility function that provies access to the Deffered // implementation and makes it very simple to use. function defer(fn) { // Create the defer instance that wraps the function var d = new Deferred(fn); // Add the defer method on the Deffered object, with the instance // bound to the queue. timeouts.push(bind(d.defer, d)); // post a message to the window that can be recieved by the // message handler. root.postMessage(messageName, "*"); // Finally return the deffered object instance. return d; } // Only return the defer function, this is all we want to be publicly // accessible. return defer; })(); // Two cache prefixes. When storing values, all keys are prefixed // to avoid collisions with other usage of the storage backend. // If the stored value is given an expire time then a second key // is set with a different prefix to store this time. LocacheCache.prototype.cachePrefix = '___locache___'; LocacheCache.prototype.expirePrefix = '___locacheExpire___'; // Built in locache backends. These are simple wrappers around the actual // storage mechanism to allow for them to be easily exchanged. LocacheCache.prototype.backends = { // Wrapper around localStorage - persistent local storage in the // browser. local: { set: function (key, value) { return root.localStorage.setItem(key, value); }, get: function (key) { return root.localStorage.getItem(key); }, remove: function (key) { return root.localStorage.removeItem(key); }, length: function (key) { return root.localStorage.length; }, key: function (index) { if (index < 0 || index >= this.length()) { return; } return root.localStorage.key(index); }, enabled: function () { return supportsNativeJSON && supportsLocalStorage; } }, // Wrapper around sessionStorage - storage in the browser that is // cleared each time a new session is started - new browser window etc. session: { set: function (key, value) { return root.sessionStorage.setItem(key, value); }, get: function (key) { return root.sessionStorage.getItem(key); }, remove: function (key) { return root.sessionStorage.removeItem(key); }, length: function (key) { return root.sessionStorage.length; }, key: function (index) { if (index < 0 || index >= this.length()) { return; } return root.sessionStorage.key(index); }, enabled: function () { return supportsNativeJSON && supportsSessionStorage; } } }; LocacheCache.prototype.storage = LocacheCache.prototype.backends.local; // Utility method to get the number of milliseconds since the Epoch. This // is used when comparing keys to see if they have expired. var _currentTime = function () { return new Date().getTime(); }; // Given a key, return the key used internally for storing values without // the risk of collisions over usage of the storage backend. LocacheCache.prototype.key = function (key) { return this.cachePrefix + key; }; // Given a key, return the key to be used internally for expiry time. LocacheCache.prototype.expirekey = function (key) { return this.expirePrefix + key; }; // Given a key, look up its expire time and determine if its in the past // or not. Returns a Boolean. LocacheCache.prototype.hasExpired = function (key) { var expireKey = this.expirekey(key); var expireValue = parseInt(this.storage.get(expireKey), 10); // If we have non-zero integer perform the comparison. if (expireValue && expireValue < _currentTime()) { return true; } return false; }; // Main public API functions. // -------------------- // Given a key, a value and an optional number of seconds store the value // in the storage backend. LocacheCache.prototype.set = function (key, value, seconds) { // If the storage backend isn't supported or the key passed in is // falsy, perform a no-op. if (!this.storage.enabled() || !key) { return; } var expireKey = this.expirekey(key); var valueKey = this.key(key); if (seconds) { // The time stored is in milliseconds, but this function expects // seconds, so multiply by 1000. var ms = seconds * 1000; this.storage.set(expireKey, _currentTime() + ms); } // For the value, always convert it into a JSON object. THis means // that we can safely store many types of objects. They still need to // be serialisable so it still rules out some, such as functions. value = JSON.stringify(value); return this.storage.set(valueKey, value); }; // Fetch a value from the cache. Either returns the value, or if it // doesn't exist (or has expired) return null. LocacheCache.prototype.get = function (key) { // If the storage backend isn't supported or the key passed in is // falsy, perform a no-op and return null. if (!this.storage.enabled() || !key) { return null; } // If the value has expired, before returning null remove the key // from the storage backend to free up the space. if (this.hasExpired(key)) { this.remove(this.key(key)); return null; } var valueKey = this.key(key); var value = this.storage.get(valueKey); // After we have the value back, check its truthy and then attempt to // parse the JSON. If the JSON parsing fails, return null. This could // be handled better but its hard to know what to do here? We only // set JSON and thus we expect JSON but we don't want to delete // values that must have come from another source. if (value) { try { return JSON.parse(value); } catch (err) { return null; } } // If value isn't truthy, it must be an empty string or similar, so // just return that. return value; }; // The async object, provides an extra level to the namespace that // contains all of the sync calls supports within locache LocacheCache.prototype.async = { set: function (key, value, seconds) { return defer(bind(function () { return this.set(key, value, seconds); }, this)); }, get: function (key) { return defer(bind(function () { return this.get(key); }, this)); } }; // When removing a key - delete from the storage both the value key/value // pair and the expiration time key/value pair. LocacheCache.prototype.remove = function (key) { // If the storage backend isn't enabled perform a no-op. if (!this.storage.enabled()) { return; } var expireKey = this.expirekey(key); var valueKey = this.key(key); this.storage.remove(expireKey); this.storage.remove(valueKey); }; // Given a key name, fetch it, increment the value and store it again. If // the counter hasn't be initialised yet, set it to zero and then perform // the increment. The fetched value is always parsed as an int to make // sure the increment will work - this means if a non-int was stored, it // will be converted first and thus reset the counter to zero. LocacheCache.prototype.incr = function (key) { // If the storage backend isn't enabled perform a no-op. if (!this.storage.enabled()) { return; } var current = parseInt(this.get(key), 10); if (!current) { current = 0; } current ++; this.set(key, current); return current; }; // Exactly the same as the incr function, but with a decrementing value. LocacheCache.prototype.decr = function (key) { // If the storage backend isn't enabled perform a no-op. if (!this.storage.enabled()) { return; } var current = parseInt(this.get(key), 10); if (!current) { current = 0; } current --; this.set(key, current); return current; }; // Given a properties object, in the form of {key: value, key:value} set // multiple keys. LocacheCache.prototype.setMany = function (properties, seconds) { // If the storage backend isn't enabled perform a no-op. if (!this.storage.enabled()) { return; } // Iterate through all the object properties. for (var key in properties) { // Ignore any inherited properties, by making sure they are in // the given objecct. if (properties.hasOwnProperty(key)) { this.set(key, properties[key], seconds); } } }; // Given an array of keys, return an array of values. If values don't // exist, null will be in their place. LocacheCache.prototype.getMany = function (keys) { var results = {}; for (var i = 0; i < keys.length; i++) { // To ensure that the correct structure is returned, if // the storage backend isn't enabled return an array of null // values with the correct length. if (this.storage.enabled()) { results[keys[i]] = this.get(keys[i]); } else { results[keys[i]] = null; } } return results; }; // Given an array of keys, return an array of values. If values don't // exist, null will be in their place. LocacheCache.prototype.getManyValues = function (keys) { var results = []; for (var i = 0; i < keys.length; i++) { // To ensure that the correct structure is returned, if // the storage backend isn't enabled return an array of null // values with the correct length. if (this.storage.enabled()) { results.push(this.get(keys[i])); } else { results.push(null); } } return results; }; // Given an array of keys, remove all of them from the cache. LocacheCache.prototype.removeMany = function (keys) { // If the storage backend isn't enabled perform a no-op. if (!this.storage.enabled()) { return; } for (var i = 0; i < keys.length; i++) { this.remove(keys[i]); } }; // Delete all stored values from the cache. This method will only remove // values added to the storage backend with the locache prefix in the key. LocacheCache.prototype.flush = function () { // If the storage backend isn't enabled perform a no-op. if (!this.storage.enabled()) { return; } var length = this.storage.length(); var prefix = this.cachePrefix; // Iteratate through all the keys stored in the storage backend - if // the key tarts with the prefix cache prefix, then remove that key. // backwards to make sure removing items does not mess up the index for (var i = length - 1; i >= 0; i--) { var key = this.storage.key(i); if (key && key.indexOf(prefix) === 0) { this.storage.remove(key); } } }; // Return the number of cache values stored in the storage backend. This // only calculates the values stored by locache LocacheCache.prototype.length = function () { // If the storage backend isn't supported perform a no-op and return // zero. if (!this.storage.enabled()) { return 0; } var c = 0; var length = this.storage.length(); var prefix = this.cachePrefix; for (var i = 0; i < length; i++) { if (this.storage.key(i).indexOf(prefix) === 0) { c++; } } return c; }; // A cleanup utility method to remove expired keys. Iterate through all // the keys stored in the storage backend. If they key is a locache key // (it has the prefix) then check to see if the key has expired. If it // has, remove the key from the cache. LocacheCache.prototype.cleanup = function () { // If the storage backend isn't enabled perform a no-op. if (!this.storage.enabled()) { return; } var length = this.storage.length(); var prefix = this.cachePrefix; for (var i = 0; i < length; i++) { var key = this.storage.key(i); // If the key matches, remove the prefix to get the original key // and then make use of the normal remove method that will clean // up the cache value key pair and the cache epiration time key // pair. if (key && key.indexOf(prefix) === 0) { var actualKey = key.substring(prefix.length, key.length); if (this.hasExpired(actualKey)) { this.remove(actualKey); } } } }; // A factory method added to the LocacheCache constructor to create // instances of itself. Rather than placing the class publicly, wrap // it up in a method and keep it for internal usage. LocacheCache.prototype.createCache = function (options) { return new LocacheCache(options); }; // The top-level instance. All public locache objects will be // attached to this object. var locache = new LocacheCache(); // To provide easy access to session caching, attack another instance of // locache to the main object. This means we can now use the full API // against sessionStorage simply by doing: `locache.session.set(...)` and // `locache.session.get(...)` locache.session = new LocacheCache({ storage: locache.backends.session }); // Attach the locache namespace to the global window object. root.locache = locache; }).call(this);