/*
* ----------------------------- 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();
})();