/* * ----------------------------- jstorage ------------------------------------- * simple local storage wrapper to save data on the browser side, supporting * all major browsers - ie6+, firefox2+, safari4+, chrome4+ and opera 10.5+ * * author: andris reinman, andris.reinman@gmail.com * project homepage: www.jstorage.info * * licensed under unlicense: * * this is free and unencumbered software released into the public domain. * * anyone is free to copy, modify, publish, use, compile, sell, or * distribute this software, either in source code form or as a compiled * binary, for any purpose, commercial or non-commercial, and by any * means. * * in jurisdictions that recognize copyright laws, the author or authors * of this software dedicate any and all copyright interest in the * software to the public domain. we make this dedication for the benefit * of the public at large and to the detriment of our heirs and * successors. we intend this dedication to be an overt act of * relinquishment in perpetuity of all present and future rights to this * software under copyright law. * * the software is provided "as is", without warranty of any kind, * express or implied, including but not limited to the warranties of * merchantability, fitness for a particular purpose and noninfringement. * in no event shall the authors be liable for any claim, damages or * other liability, whether in an action of contract, tort or otherwise, * arising from, out of or in connection with the software or the use or * other dealings in the software. * * for more information, please refer to */ (function(){ var /* jstorage version */ jstorage_version = "0.4.7", /* detect a dollar object or create one if not found */ $ = window.jquery || window.$ || (window.$ = {}), /* check for a json handling support */ json = { parse: window.json && (window.json.parse || window.json.decode) || string.prototype.evaljson && function(str){return string(str).evaljson();} || $.parsejson || $.evaljson, stringify: object.tojson || window.json && (window.json.stringify || window.json.encode) || $.tojson }; // break if no json support was found if(!("parse" in json) || !("stringify" in json)){ throw new error("no json support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page"); } var /* this is the object, that holds the cached values */ _storage = {__jstorage_meta:{crc32:{}}}, /* actual browser storage (localstorage or globalstorage["domain"]) */ _storage_service = {jstorage:"{}"}, /* dom element for older ie versions, holds userdata behavior */ _storage_elm = null, /* how much space does the storage take */ _storage_size = 0, /* which backend is currently used */ _backend = false, /* onchange observers */ _observers = {}, /* timeout to wait after onchange event */ _observer_timeout = false, /* last update time */ _observer_update = 0, /* pubsub observers */ _pubsub_observers = {}, /* skip published items older than current timestamp */ _pubsub_last = +new date(), /* next check for ttl */ _ttl_timeout, /** * xml encoding and decoding as xml nodes can't be json'ized * xml nodes are encoded and decoded if the node is the value to be saved * but not if it's as a property of another object * eg. - * $.jstorage.set("key", xmlnode); // is ok * $.jstorage.set("key", {xml: xmlnode}); // not ok */ _xmlservice = { /** * validates a xml node to be xml * based on jquery.isxml function */ isxml: function(elm){ var documentelement = (elm ? elm.ownerdocument || elm : 0).documentelement; return documentelement ? documentelement.nodename !== "html" : false; }, /** * encodes a xml node to string * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/ */ encode: function(xmlnode) { if(!this.isxml(xmlnode)){ return false; } try{ // mozilla, webkit, opera return new xmlserializer().serializetostring(xmlnode); }catch(e1) { try { // ie return xmlnode.xml; }catch(e2){} } return false; }, /** * decodes a xml node from string * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/ */ decode: function(xmlstring){ var dom_parser = ("domparser" in window && (new domparser()).parsefromstring) || (window.activexobject && function(_xmlstring) { var xml_doc = new activexobject("microsoft.xmldom"); xml_doc.async = "false"; xml_doc.loadxml(_xmlstring); return xml_doc; }), resultxml; if(!dom_parser){ return false; } resultxml = dom_parser.call("domparser" in window && (new domparser()) || window, xmlstring, "text/xml"); return this.isxml(resultxml)?resultxml:false; } }; ////////////////////////// private methods //////////////////////// /** * initialization function. detects if the browser supports dom storage * or userdata behavior and behaves accordingly. */ function _init(){ /* check if browser supports localstorage */ var localstoragereallyworks = false; if("localstorage" in window){ try { window.localstorage.setitem("_tmptest", "tmpval"); localstoragereallyworks = true; window.localstorage.removeitem("_tmptest"); } catch(bogusquotaexceedederroronios5) { // thanks be to ios5 private browsing mode which throws // quota_exceeded_errror dom exception 22. } } if(localstoragereallyworks){ try { if(window.localstorage) { _storage_service = window.localstorage; _backend = "localstorage"; _observer_update = _storage_service.jstorage_update; } } catch(e3) {/* firefox fails when touching localstorage and cookies are disabled */} } /* check if browser supports globalstorage */ else if("globalstorage" in window){ try { if(window.globalstorage) { if(window.location.hostname == "localhost"){ _storage_service = window.globalstorage["localhost.localdomain"]; } else{ _storage_service = window.globalstorage[window.location.hostname]; } _backend = "globalstorage"; _observer_update = _storage_service.jstorage_update; } } catch(e4) {/* firefox fails when touching localstorage and cookies are disabled */} } /* check if browser supports userdata behavior */ else { _storage_elm = document.createelement("link"); if(_storage_elm.addbehavior){ /* use a dom element to act as userdata storage */ _storage_elm.style.behavior = "url(#default#userdata)"; /* userdata element needs to be inserted into the dom! */ document.getelementsbytagname("head")[0].appendchild(_storage_elm); try{ _storage_elm.load("jstorage"); }catch(e){ // try to reset cache _storage_elm.setattribute("jstorage", "{}"); _storage_elm.save("jstorage"); _storage_elm.load("jstorage"); } var data = "{}"; try{ data = _storage_elm.getattribute("jstorage"); }catch(e5){} try{ _observer_update = _storage_elm.getattribute("jstorage_update"); }catch(e6){} _storage_service.jstorage = data; _backend = "userdatabehavior"; }else{ _storage_elm = null; return; } } // load data from storage _load_storage(); // remove dead keys _handlettl(); // start listening for changes _setupobserver(); // initialize publish-subscribe service _handlepubsub(); // handle cached navigation if("addeventlistener" in window){ window.addeventlistener("pageshow", function(event){ if(event.persisted){ _storageobserver(); } }, false); } } /** * reload data from storage when needed */ function _reloaddata(){ var data = "{}"; if(_backend == "userdatabehavior"){ _storage_elm.load("jstorage"); try{ data = _storage_elm.getattribute("jstorage"); }catch(e5){} try{ _observer_update = _storage_elm.getattribute("jstorage_update"); }catch(e6){} _storage_service.jstorage = data; } _load_storage(); // remove dead keys _handlettl(); _handlepubsub(); } /** * sets up a storage change observer */ function _setupobserver(){ if(_backend == "localstorage" || _backend == "globalstorage"){ if("addeventlistener" in window){ window.addeventlistener("storage", _storageobserver, false); }else{ document.attachevent("onstorage", _storageobserver); } }else if(_backend == "userdatabehavior"){ setinterval(_storageobserver, 1000); } } /** * fired on any kind of data change, needs to check if anything has * really been changed */ function _storageobserver(){ var updatetime; // cumulate change notifications with timeout cleartimeout(_observer_timeout); _observer_timeout = settimeout(function(){ if(_backend == "localstorage" || _backend == "globalstorage"){ updatetime = _storage_service.jstorage_update; }else if(_backend == "userdatabehavior"){ _storage_elm.load("jstorage"); try{ updatetime = _storage_elm.getattribute("jstorage_update"); }catch(e5){} } if(updatetime && updatetime != _observer_update){ _observer_update = updatetime; _checkupdatedkeys(); } }, 25); } /** * reloads the data and checks if any keys are changed */ function _checkupdatedkeys(){ var oldcrc32list = json.parse(json.stringify(_storage.__jstorage_meta.crc32)), newcrc32list; _reloaddata(); newcrc32list = json.parse(json.stringify(_storage.__jstorage_meta.crc32)); var key, updated = [], removed = []; for(key in oldcrc32list){ if(oldcrc32list.hasownproperty(key)){ if(!newcrc32list[key]){ removed.push(key); continue; } if(oldcrc32list[key] != newcrc32list[key] && string(oldcrc32list[key]).substr(0,2) == "2."){ updated.push(key); } } } for(key in newcrc32list){ if(newcrc32list.hasownproperty(key)){ if(!oldcrc32list[key]){ updated.push(key); } } } _fireobservers(updated, "updated"); _fireobservers(removed, "deleted"); } /** * fires observers for updated keys * * @param {array|string} keys array of key names or a key * @param {string} action what happened with the value (updated, deleted, flushed) */ function _fireobservers(keys, action){ keys = [].concat(keys || []); if(action == "flushed"){ keys = []; for(var key in _observers){ if(_observers.hasownproperty(key)){ keys.push(key); } } action = "deleted"; } for(var i=0, len = keys.length; i=0; i--){ pubelm = _storage.__jstorage_meta.pubsub[i]; if(pubelm[0] > _pubsub_last){ _pubsubcurrent = pubelm[0]; _firesubscribers(pubelm[1], pubelm[2]); } } _pubsub_last = _pubsubcurrent; } /** * fires all subscriber listeners for a pubsub channel * * @param {string} channel channel name * @param {mixed} payload payload data to deliver */ function _firesubscribers(channel, payload){ if(_pubsub_observers[channel]){ for(var i=0, len = _pubsub_observers[channel].length; igary court * @see http://github.com/garycourt/murmurhash-js * @author austin appleby * @see http://sites.google.com/site/murmurhash/ * * @param {string} str ascii only * @param {number} seed positive integer only * @return {number} 32-bit positive integer hash */ function murmurhash2_32_gc(str, seed) { var l = str.length, h = seed ^ l, i = 0, k; while (l >= 4) { k = ((str.charcodeat(i) & 0xff)) | ((str.charcodeat(++i) & 0xff) << 8) | ((str.charcodeat(++i) & 0xff) << 16) | ((str.charcodeat(++i) & 0xff) << 24); k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); k ^= k >>> 24; k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16)); h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k; l -= 4; ++i; } switch (l) { case 3: h ^= (str.charcodeat(i + 2) & 0xff) << 16; case 2: h ^= (str.charcodeat(i + 1) & 0xff) << 8; case 1: h ^= (str.charcodeat(i) & 0xff); h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); } h ^= h >>> 13; h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)); h ^= h >>> 15; return h >>> 0; } ////////////////////////// public interface ///////////////////////// $.jstorage = { /* version number */ version: jstorage_version, /** * sets a key's value. * * @param {string} key key to set. if this value is not set or not * a string an exception is raised. * @param {mixed} value value to set. this can be any value that is json * compatible (numbers, strings, objects etc.). * @param {object} [options] - possible options to use * @param {number} [options.ttl] - optional ttl value * @return {mixed} the used value */ set: function(key, value, options){ _checkkey(key); options = options || {}; // undefined values are deleted automatically if(typeof value == "undefined"){ this.deletekey(key); return value; } if(_xmlservice.isxml(value)){ value = {_is_xml:true,xml:_xmlservice.encode(value)}; }else if(typeof value == "function"){ return undefined; // functions can't be saved! }else if(value && typeof value == "object"){ // clone the object before saving to _storage tree value = json.parse(json.stringify(value)); } _storage[key] = value; _storage.__jstorage_meta.crc32[key] = "2." + murmurhash2_32_gc(json.stringify(value), 0x9747b28c); this.setttl(key, options.ttl || 0); // also handles saving and _publishchange _fireobservers(key, "updated"); return value; }, /** * looks up a key in cache * * @param {string} key - key to look up. * @param {mixed} def - default value to return, if key didn't exist. * @return {mixed} the key value, default value or null */ get: function(key, def){ _checkkey(key); if(key in _storage){ if(_storage[key] && typeof _storage[key] == "object" && _storage[key]._is_xml) { return _xmlservice.decode(_storage[key].xml); }else{ return _storage[key]; } } return typeof(def) == "undefined" ? null : def; }, /** * deletes a key from cache. * * @param {string} key - key to delete. * @return {boolean} true if key existed or false if it didn't */ deletekey: function(key){ _checkkey(key); if(key in _storage){ delete _storage[key]; // remove from ttl list if(typeof _storage.__jstorage_meta.ttl == "object" && key in _storage.__jstorage_meta.ttl){ delete _storage.__jstorage_meta.ttl[key]; } delete _storage.__jstorage_meta.crc32[key]; _save(); _publishchange(); _fireobservers(key, "deleted"); return true; } return false; }, /** * sets a ttl for a key, or remove it if ttl value is 0 or below * * @param {string} key - key to set the ttl for * @param {number} ttl - ttl timeout in milliseconds * @return {boolean} true if key existed or false if it didn't */ setttl: function(key, ttl){ var curtime = +new date(); _checkkey(key); ttl = number(ttl) || 0; if(key in _storage){ if(!_storage.__jstorage_meta.ttl){ _storage.__jstorage_meta.ttl = {}; } // set ttl value for the key if(ttl>0){ _storage.__jstorage_meta.ttl[key] = curtime + ttl; }else{ delete _storage.__jstorage_meta.ttl[key]; } _save(); _handlettl(); _publishchange(); return true; } return false; }, /** * gets remaining ttl (in milliseconds) for a key or 0 when no ttl has been set * * @param {string} key key to check * @return {number} remaining ttl in milliseconds */ getttl: function(key){ var curtime = +new date(), ttl; _checkkey(key); if(key in _storage && _storage.__jstorage_meta.ttl && _storage.__jstorage_meta.ttl[key]){ ttl = _storage.__jstorage_meta.ttl[key] - curtime; return ttl || 0; } return 0; }, /** * deletes everything in cache. * * @return {boolean} always true */ flush: function(){ _storage = {__jstorage_meta:{crc32:{}}}; _save(); _publishchange(); _fireobservers(null, "flushed"); return true; }, /** * returns a read-only copy of _storage * * @return {object} read-only copy of _storage */ storageobj: function(){ function f() {} f.prototype = _storage; return new f(); }, /** * returns an index of all used keys as an array * ["key1", "key2",.."keyn"] * * @return {array} used keys */ index: function(){ var index = [], i; for(i in _storage){ if(_storage.hasownproperty(i) && i != "__jstorage_meta"){ index.push(i); } } return index; }, /** * how much space in bytes does the storage take? * * @return {number} storage size in chars (not the same as in bytes, * since some chars may take several bytes) */ storagesize: function(){ return _storage_size; }, /** * which backend is currently in use? * * @return {string} backend name */ currentbackend: function(){ return _backend; }, /** * test if storage is available * * @return {boolean} true if storage can be used */ storageavailable: function(){ return !!_backend; }, /** * register change listeners * * @param {string} key key name * @param {function} callback function to run when the key changes */ listenkeychange: function(key, callback){ _checkkey(key); if(!_observers[key]){ _observers[key] = []; } _observers[key].push(callback); }, /** * remove change listeners * * @param {string} key key name to unregister listeners against * @param {function} [callback] if set, unregister the callback, if not - unregister all */ stoplistening: function(key, callback){ _checkkey(key); if(!_observers[key]){ return; } if(!callback){ delete _observers[key]; return; } for(var i = _observers[key].length - 1; i>=0; i--){ if(_observers[key][i] == callback){ _observers[key].splice(i,1); } } }, /** * subscribe to a publish/subscribe event stream * * @param {string} channel channel name * @param {function} callback function to run when the something is published to the channel */ subscribe: function(channel, callback){ channel = (channel || "").tostring(); if(!channel){ throw new typeerror("channel not defined"); } if(!_pubsub_observers[channel]){ _pubsub_observers[channel] = []; } _pubsub_observers[channel].push(callback); }, /** * publish data to an event stream * * @param {string} channel channel name * @param {mixed} payload payload to deliver */ publish: function(channel, payload){ channel = (channel || "").tostring(); if(!channel){ throw new typeerror("channel not defined"); } _publish(channel, payload); }, /** * reloads the data from browser storage */ reinit: function(){ _reloaddata(); }, /** * removes reference from global objects and saves it as jstorage * * @param {boolean} option if needed to save object as simple "jstorage" in windows context */ noconflict: function( saveinglobal ) { delete window.$.jstorage if ( saveinglobal ) { window.jstorage = this; } return this; } }; // initialize jstorage _init(); })();