Merge "Minor bugfix to IP::prettifyIP."
[lhc/web/wiklou.git] / resources / jquery / jquery.jStorage.js
index 95959cf..6ca21b5 100644 (file)
@@ -3,12 +3,9 @@
  * Simple local storage wrapper to save data on the browser side, supporting
  * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
  *
- * Copyright (c) 2010 Andris Reinman, andris.reinman@gmail.com
+ * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com
  * Project homepage: www.jstorage.info
  *
- * Taken from Github with slight modifications by Hoo man
- * https://raw.github.com/andris9/jStorage/master/jstorage.js
- *
  * Licensed under MIT-style license:
  *
  * Permission is hereby granted, free of charge, to any person obtaining a copy
  * SOFTWARE.
  */
 
-/**
- * $.jStorage
- *
- * USAGE:
- *
- * jStorage requires Prototype, MooTools or jQuery! If jQuery is used, then
- * jQuery-JSON (http://code.google.com/p/jquery-json/) is also needed.
- * (jQuery-JSON needs to be loaded BEFORE jStorage!)
- *
- * Methods:
- *
- * -set(key, value[, options])
- * $.jStorage.set(key, value) -> saves a value
- *
- * -get(key[, default])
- * value = $.jStorage.get(key [, default]) ->
- *    retrieves value if key exists, or default if it doesn't
- *
- * -deleteKey(key)
- * $.jStorage.deleteKey(key) -> removes a key from the storage
- *
- * -flush()
- * $.jStorage.flush() -> clears the cache
- *
- * -storageObj()
- * $.jStorage.storageObj() -> returns a read-ony copy of the actual storage
- *
- * -storageSize()
- * $.jStorage.storageSize() -> returns the size of the storage in bytes
- *
- * -index()
- * $.jStorage.index() -> returns the used keys as an array
- *
- * -storageAvailable()
- * $.jStorage.storageAvailable() -> returns true if storage is available
- *
- * -reInit()
- * $.jStorage.reInit() -> reloads the data from browser storage
- *
- * <value> can be any JSON-able value, including objects and arrays.
- *
- **/
+ (function(){
+    var
+        /* jStorage version */
+        JSTORAGE_VERSION = "0.3.0",
+
+        /* detect a dollar object or create one if not found */
+        $ = window.jQuery || window.$ || (window.$ = {}),
 
-(function($){
-    if(!$ || !($.toJSON || Object.toJSON || window.JSON)){
-        throw new Error("jQuery, MooTools or Prototype needs to be loaded before jStorage!");
+        /* 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(!JSON.parse || !JSON.stringify){
+        throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page");
     }
 
     var
         /* How much space does the storage take */
         _storage_size = 0,
 
-        /* function to encode objects to JSON strings */
-        json_encode = $.toJSON || Object.toJSON || (window.JSON && (JSON.encode || JSON.stringify)),
-
-        /* function to decode objects from JSON strings */
-        json_decode = $.evalJSON || (window.JSON && (JSON.decode || JSON.parse)) || function(str){
-            return String(str).evalJSON();
-        },
-
         /* 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,
 
+        /* crc32 table */
+        _crc32Table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 "+
+             "0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 "+
+             "6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 "+
+             "FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 "+
+             "A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 "+
+             "32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 "+
+             "56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 "+
+             "C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 "+
+             "E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 "+
+             "6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 "+
+             "12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE "+
+             "A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 "+
+             "DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 "+
+             "5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 "+
+             "2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF "+
+             "04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 "+
+             "7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 "+
+             "FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 "+
+             "A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C "+
+             "36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 "+
+             "5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 "+
+             "C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 "+
+             "EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D "+
+             "7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 "+
+             "18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 "+
+             "A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A "+
+             "D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A "+
+             "53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 "+
+             "2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D",
+
         /**
          * 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
                 resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml');
                 return this.isXML(resultXML)?resultXML:false;
             }
-        };
+        },
+
+        _localStoragePolyfillSetKey = function(){};
+
 
     ////////////////////////// PRIVATE METHODS ////////////////////////
 
     /**
      * Initialization function. Detects if the browser supports DOM Storage
      * or userData behavior and behaves accordingly.
-     * @returns undefined
      */
     function _init(){
         /* Check if browser supports localStorage */
                 // 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 */}
         }
                 if(window.globalStorage) {
                     _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 */}
         }
                 /* userData element needs to be inserted into the DOM! */
                 document.getElementsByTagName('head')[0].appendChild(_storage_elm);
 
-                _storage_elm.load("jStorage");
+                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{
             }
         }
 
+        // Load data from storage
+        _load_storage();
+
+        // remove dead keys
+        _handleTTL();
+
+        // create localStorage and sessionStorage polyfills if needed
+        _createPolyfillStorage("local");
+        _createPolyfillStorage("session");
+
+        // 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);
+        }
+    }
+
+    /**
+     * Create a polyfill for localStorage (type="local") or sessionStorage (type="session")
+     *
+     * @param {String} type Either "local" or "session"
+     * @param {Boolean} forceCreate If set to true, recreate the polyfill (needed with flush)
+     */
+    function _createPolyfillStorage(type, forceCreate){
+        var _skipSave = false,
+            _length = 0,
+            i, 
+            storage,
+            storage_source = {};
+
+            var rand = Math.random();
+
+        if(!forceCreate && typeof window[type+"Storage"] != "undefined"){
+            return;
+        }
+
+        // Use globalStorage for localStorage if available
+        if(type == "local" && window.globalStorage){
+            localStorage = window.globalStorage[window.location.hostname];
+            return;
+        }
+
+        // only IE6/7 from this point on 
+        if(_backend != "userDataBehavior"){
+            return;
+        }
+
+        // Remove existing storage element if available
+        if(forceCreate && window[type+"Storage"] && window[type+"Storage"].parentNode){
+            window[type+"Storage"].parentNode.removeChild(window[type+"Storage"]);
+        }
+
+        storage = document.createElement("button");
+        document.getElementsByTagName('head')[0].appendChild(storage);
+
+        if(type == "local"){
+            storage_source = _storage;
+        }else if(type == "session"){
+            _sessionStoragePolyfillUpdate();
+        }
+
+        for(i in storage_source){
+
+            if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i != "length" && typeof storage_source[i] != "undefined"){
+                if(!(i in storage)){
+                    _length++;
+                }
+                storage[i] = storage_source[i];
+            }
+        }
+        
+        // Polyfill API
+
+        /**
+         * Indicates how many keys are stored in the storage
+         */
+        storage.length = _length;
+
+        /**
+         * Returns the key of the nth stored value
+         * 
+         * @param {Number} n Index position
+         * @return {String} Key name of the nth stored value
+         */
+        storage.key = function(n){
+            var count = 0, i;
+            _sessionStoragePolyfillUpdate();
+            for(i in storage_source){
+                if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i!="length" && typeof storage_source[i] != "undefined"){
+                    if(count == n){
+                        return i;
+                    }
+                    count++;
+                }
+            }
+        }
+
+        /**
+         * Returns the current value associated with the given key
+         *
+         * @param {String} key key name
+         * @return {Mixed} Stored value
+         */
+        storage.getItem = function(key){
+            _sessionStoragePolyfillUpdate();
+            if(type == "session"){
+                return storage_source[key];
+            }
+            return $.jStorage.get(key);
+        }
+
+        /**
+         * Sets or updates value for a give key
+         *
+         * @param {String} key Key name to be updated
+         * @param {String} value String value to be stored 
+         */
+        storage.setItem = function(key, value){
+            if(typeof value == "undefined"){
+                return;
+            }
+            storage[key] = (value || "").toString();
+        }
+
+        /**
+         * Removes key from the storage
+         *
+         * @param {String} key Key name to be removed
+         */
+        storage.removeItem = function(key){
+            if(type == "local"){
+                return $.jStorage.deleteKey(key);
+            }
+
+            storage[key] = undefined;
+            
+            _skipSave = true;
+            if(key in storage){
+                storage.removeAttribute(key);
+            }
+            _skipSave = false;
+        }
+
+        /**
+         * Clear storage
+         */
+        storage.clear = function(){
+            if(type == "session"){
+                window.name = "";
+                _createPolyfillStorage("session", true);
+                return;
+            }
+            $.jStorage.flush();
+        }
+
+        if(type == "local"){
+
+            _localStoragePolyfillSetKey = function(key, value){
+                if(key == "length"){
+                    return;
+                }
+                _skipSave = true;
+                if(typeof value == "undefined"){
+                    if(key in storage){
+                        _length--;
+                        storage.removeAttribute(key);
+                    }
+                }else{
+                    if(!(key in storage)){
+                        _length++;
+                    }
+                    storage[key] = (value || "").toString();
+                }
+                storage.length = _length;
+                _skipSave = false;
+            }
+        }
+
+        function _sessionStoragePolyfillUpdate(){
+                if(type != "session"){
+                    return;
+                }
+                try{
+                    storage_source = JSON.parse(window.name || "{}");
+                }catch(E){
+                    storage_source = {};
+                }
+            }
+
+        function _sessionStoragePolyfillSave(){
+            if(type != "session"){
+                return;
+            }
+            window.name = JSON.stringify(storage_source);
+        };
+
+        storage.attachEvent("onpropertychange", function(e){
+            if(e.propertyName == "length"){
+                return;
+            }
+
+            if(_skipSave || e.propertyName == "length"){
+                return;
+            }
+
+            if(type == "local"){
+                if(!(e.propertyName in storage_source) && typeof storage[e.propertyName] != "undefined"){
+                    _length ++;
+                }
+            }else if(type == "session"){
+                _sessionStoragePolyfillUpdate();
+                if(typeof storage[e.propertyName] != "undefined" && !(e.propertyName in storage_source)){
+                    storage_source[e.propertyName] = storage[e.propertyName];
+                    _length++;
+                }else if(typeof storage[e.propertyName] == "undefined" && e.propertyName in storage_source){
+                    delete storage_source[e.propertyName];
+                    _length--;
+                }else{
+                    storage_source[e.propertyName] = storage[e.propertyName];
+                }
+
+                _sessionStoragePolyfillSave();
+                storage.length = _length;
+                return;
+            }
+
+            $.jStorage.set(e.propertyName, storage[e.propertyName]);
+            storage.length = _length;
+        });
+
+        window[type+"Storage"] = storage;
+    }
+
+    /**
+     * 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]){
+                    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<len; i++){
+            if(_observers[keys[i]]){
+                for(var j=0, jlen = _observers[keys[i]].length; j<jlen; j++){
+                    _observers[keys[i]][j](keys[i], action);
+                }
+            }
+        }
+    }
+
+    /**
+     * Publishes key change to listeners
+     */
+    function _publishChange(){
+        var updateTime = (+new Date()).toString();
+
+        if(_backend == "localStorage" || _backend == "globalStorage"){
+            _storage_service.jStorage_update = updateTime;
+        }else if(_backend == "userDataBehavior"){
+            _storage_elm.setAttribute("jStorage_update", updateTime);
+            _storage_elm.save("jStorage");
+        }
+
+        _storageObserver();
     }
 
     /**
      * Loads the data from the storage based on the supported mechanism
-     * @returns undefined
      */
     function _load_storage(){
         /* if jStorage string is retrieved, then decode it */
         if(_storage_service.jStorage){
             try{
-                _storage = json_decode(String(_storage_service.jStorage));
+                _storage = JSON.parse(String(_storage_service.jStorage));
             }catch(E6){_storage_service.jStorage = "{}";}
         }else{
             _storage_service.jStorage = "{}";
         }
         _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
+
+        if(!_storage.__jstorage_meta){
+            _storage.__jstorage_meta = {};
+        }
+        if(!_storage.__jstorage_meta.CRC32){
+            _storage.__jstorage_meta.CRC32 = {};
+        }
     }
 
     /**
      * This functions provides the "save" mechanism to store the jStorage object
-     * @returns undefined
      */
     function _save(){
+        _dropOldEvents(); // remove expired events
         try{
-            _storage_service.jStorage = json_encode(_storage);
+            _storage_service.jStorage = JSON.stringify(_storage);
             // If userData is used as the storage engine, additional
             if(_storage_elm) {
                 _storage_elm.setAttribute("jStorage",_storage_service.jStorage);
 
     /**
      * Function checks if a key is set and is string or numberic
+     *
+     * @param {String} key Key name
      */
     function _checkKey(key){
-        if(!key || (typeof key !== "string" && typeof key !== "number")){
+        if(!key || (typeof key != "string" && typeof key != "number")){
             throw new TypeError('Key name must be string or numeric');
         }
-        if(key === "__jstorage_meta"){
+        if(key == "__jstorage_meta"){
             throw new TypeError('Reserved key name');
         }
         return true;
      * Removes expired keys
      */
     function _handleTTL(){
-        var curtime, i, TTL, nextExpire = Infinity, changed = false;
+        var curtime, i, TTL, CRC32, nextExpire = Infinity, changed = false, deleted = [];
 
         clearTimeout(_ttl_timeout);
 
-        if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL !== "object"){
+        if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object"){
             // nothing to do here
             return;
         }
 
         curtime = +new Date();
         TTL = _storage.__jstorage_meta.TTL;
+
+        CRC32 = _storage.__jstorage_meta.CRC32;
         for(i in TTL){
             if(TTL.hasOwnProperty(i)){
                 if(TTL[i] <= curtime){
                     delete TTL[i];
+                    delete CRC32[i];
                     delete _storage[i];
                     changed = true;
+                    deleted.push(i);
                 }else if(TTL[i] < nextExpire){
                     nextExpire = TTL[i];
                 }
         // save changes
         if(changed){
             _save();
+            _publishChange();
+            _fireObservers(deleted, "deleted");
         }
     }
 
+    /**
+     * Checks if there's any events on hold to be fired to listeners
+     */
+    function _handlePubSub(){
+        if(!_storage.__jstorage_meta.PubSub){
+            return;
+        }
+        var pubelm,
+            _pubsubCurrent = _pubsub_last;
+
+        for(var i=len=_storage.__jstorage_meta.PubSub.length-1; 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; i<len; i++){
+                // send immutable data that can't be modified by listeners
+                _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
+            }
+        }
+    }
+
+    /**
+     * Remove old events from the publish stream (at least 2sec old)
+     */
+    function _dropOldEvents(){
+        if(!_storage.__jstorage_meta.PubSub){
+            return;
+        }
+
+        var retire = +new Date() - 2000;
+
+        for(var i=0, len = _storage.__jstorage_meta.PubSub.length; i<len; i++){
+            if(_storage.__jstorage_meta.PubSub[i][0] <= retire){
+                // deleteCount is needed for IE6
+                _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
+                break;
+            }
+        }
+
+        if(!_storage.__jstorage_meta.PubSub.length){
+            delete _storage.__jstorage_meta.PubSub;
+        }
+
+    }
+
+    /**
+     * Publish payload to a channel
+     *
+     * @param {String} channel Channel name
+     * @param {Mixed} payload Payload to send to the subscribers
+     */
+    function _publish(channel, payload){
+        if(!_storage.__jstorage_meta){
+            _storage.__jstorage_meta = {};
+        }
+        if(!_storage.__jstorage_meta.PubSub){
+            _storage.__jstorage_meta.PubSub = [];
+        }
+        
+        _storage.__jstorage_meta.PubSub.unshift([+new Date, channel, payload]);
+
+        _save();
+        _publishChange();
+    }
+
+    /**
+     * CRC32 calculation based on http://noteslog.com/post/crc32-for-javascript/
+     *
+     * @param {String} str String to be hashed
+     * @param {Number} [crc] Last crc value in case of streams
+     */
+    function _crc32(str, crc){
+        crc = crc || 0;
+
+        var n = 0, //a number between 0 and 255
+            x = 0; //an hex number
+        crc = crc ^ (-1);
+        for(var i = 0, len = str.length; i < len; i++){
+            n = (crc ^ str.charCodeAt(i)) & 0xFF;
+            x = "0x" + _crc32Table.substr(n * 9, 8);
+            crc = (crc >>> 8)^x;
+        }
+        return crc^(-1);
+    }
+
     ////////////////////////// PUBLIC INTERFACE /////////////////////////
 
     $.jStorage = {
         /* Version number */
-        version: "0.1.7.0",
+        version: JSTORAGE_VERSION,
 
         /**
          * Sets a key's value.
          *
-         * @param {String} key Key to set. If this value is not set or not
+         * @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
+         * @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
-         * @returns the used 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"){
-                value = null; // functions can't be saved!
-            }else if(value && typeof value === "object"){
+            }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_decode(json_encode(value));
+                value = JSON.parse(JSON.stringify(value));
             }
+
             _storage[key] = value;
 
-            if(!isNaN(options.TTL)){
-                this.setTTL(key, options.TTL);
-                // also handles saving
-            }else{
-                _save();
-            }
+            _storage.__jstorage_meta.CRC32[key] = _crc32(JSON.stringify(value));
+
+            this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
+
+            _localStoragePolyfillSetKey(key, value);
+
+            _fireObservers(key, "updated");
             return value;
         },
 
          *
          * @param {String} key - Key to look up.
          * @param {mixed} def - Default value to return, if key didn't exist.
-         * @returns the key value, default value or <null>
+         * @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" &&
+                if(_storage[key] && typeof _storage[key] == "object" &&
                         _storage[key]._is_xml &&
                             _storage[key]._is_xml){
                     return _XMLService.decode(_storage[key].xml);
                     return _storage[key];
                 }
             }
-            return typeof(def) === 'undefined' ? null : def;
+            return typeof(def) == 'undefined' ? null : def;
         },
 
         /**
          * Deletes a key from cache.
          *
          * @param {String} key - Key to delete.
-         * @returns true if key existed or false if it didn't
+         * @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(_storage.__jstorage_meta &&
-                  typeof _storage.__jstorage_meta.TTL === "object" &&
+                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];
+                _localStoragePolyfillSetKey(key, undefined);
+
                 _save();
+                _publishChange();
+                _fireObservers(key, "deleted");
                 return true;
             }
             return false;
          *
          * @param {String} key - key to set the TTL for
          * @param {Number} ttl - TTL timeout in milliseconds
-         * @returns true if key existed or false if it didn't
+         * @return {Boolean} true if key existed or false if it didn't
          */
         setTTL: function(key, ttl){
             var curtime = +new Date();
             ttl = Number(ttl) || 0;
             if(key in _storage){
 
-                if(!_storage.__jstorage_meta){
-                    _storage.__jstorage_meta = {};
-                }
                 if(!_storage.__jstorage_meta.TTL){
                     _storage.__jstorage_meta.TTL = {};
                 }
                 _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 true
+         * @return {Boolean} Always true
          */
         flush: function(){
-            _storage = {};
+            _storage = {__jstorage_meta:{CRC32:{}}};
+            _createPolyfillStorage("local", true);
             _save();
+            _publishChange();
+            _fireObservers(null, "flushed");
             return true;
         },
 
         /**
          * Returns a read-only copy of _storage
          *
-         * @returns Object
+         * @return {Object} Read-only copy of _storage
         */
         storageObj: function(){
             function F() {}
          * Returns an index of all used keys as an array
          * ['key1', 'key2',..'keyN']
          *
-         * @returns Array
+         * @return {Array} Used keys
         */
         index: function(){
             var index = [], i;
             for(i in _storage){
-                if(_storage.hasOwnProperty(i) && i !== "__jstorage_meta"){
+                if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){
                     index.push(i);
                 }
             }
         /**
          * How much space in bytes does the storage take?
          *
-         * @returns Number
+         * @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?
          *
-         * @returns String
+         * @return {String} Backend name
          */
         currentBackend: function(){
             return _backend;
         /**
          * Test if storage is available
          *
-         * @returns Boolean
+         * @return {Boolean} True if storage can be used
          */
         storageAvailable: function(){
             return !!_backend;
         },
 
         /**
-         * Reloads the data from browser storage
+         * Register change listeners
          *
-         * @returns undefined
+         * @param {String} key Key name
+         * @param {Function} callback Function to run when the key changes
          */
-        reInit: function(){
-            var new_storage_elm, data;
-            if(_storage_elm && _storage_elm.addBehavior){
-                new_storage_elm = document.createElement('link');
+        listenKeyChange: function(key, callback){
+            _checkKey(key);
+            if(!_observers[key]){
+                _observers[key] = [];
+            }
+            _observers[key].push(callback);
+        },
 
-                _storage_elm.parentNode.replaceChild(new_storage_elm, _storage_elm);
-                _storage_elm = new_storage_elm;
+        /**
+         * 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);
 
-                /* Use a DOM element to act as userData storage */
-                _storage_elm.style.behavior = 'url(#default#userData)';
+            if(!_observers[key]){
+                return;
+            }
 
-                /* userData element needs to be inserted into the DOM! */
-                document.getElementsByTagName('head')[0].appendChild(_storage_elm);
+            if(!callback){
+                delete _observers[key];
+                return;
+            }
 
-                _storage_elm.load("jStorage");
-                data = "{}";
-                try{
-                    data = _storage_elm.getAttribute("jStorage");
-                }catch(E5){}
-                _storage_service.jStorage = data;
-                _backend = "userDataBehavior";
+            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);
+        },
 
-            _load_storage();
+        /**
+         * 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();
         }
     };
 
     // Initialize jStorage
     _init();
 
-})(window.$ || window.jQuery);
+})();