Merge "Cleanups to WebRequest::getIP logic"
[mediawiki.git] / resources / lib / jquery / jquery.jStorage.js
blob324833c92efb7aacd05d36c84d984233477779bc
1 /*
2  * ----------------------------- JSTORAGE -------------------------------------
3  * Simple local storage wrapper to save data on the browser side, supporting
4  * all major browsers - IE6+, Firefox2+, Safari4+, Chrome4+ and Opera 10.5+
5  *
6  * Author: Andris Reinman, andris.reinman@gmail.com
7  * Project homepage: www.jstorage.info
8  *
9  * Licensed under Unlicense:
10  *
11  * This is free and unencumbered software released into the public domain.
12  * 
13  * Anyone is free to copy, modify, publish, use, compile, sell, or
14  * distribute this software, either in source code form or as a compiled
15  * binary, for any purpose, commercial or non-commercial, and by any
16  * means.
17  * 
18  * In jurisdictions that recognize copyright laws, the author or authors
19  * of this software dedicate any and all copyright interest in the
20  * software to the public domain. We make this dedication for the benefit
21  * of the public at large and to the detriment of our heirs and
22  * successors. We intend this dedication to be an overt act of
23  * relinquishment in perpetuity of all present and future rights to this
24  * software under copyright law.
25  * 
26  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
27  * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
28  * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
29  * IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR
30  * OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE,
31  * ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
32  * OTHER DEALINGS IN THE SOFTWARE.
33  * 
34  * For more information, please refer to <http://unlicense.org/>
35  */
37  (function(){
38     var
39         /* jStorage version */
40         JSTORAGE_VERSION = "0.4.8",
42         /* detect a dollar object or create one if not found */
43         $ = window.jQuery || window.$ || (window.$ = {}),
45         /* check for a JSON handling support */
46         JSON = {
47             parse:
48                 window.JSON && (window.JSON.parse || window.JSON.decode) ||
49                 String.prototype.evalJSON && function(str){return String(str).evalJSON();} ||
50                 $.parseJSON ||
51                 $.evalJSON,
52             stringify:
53                 Object.toJSON ||
54                 window.JSON && (window.JSON.stringify || window.JSON.encode) ||
55                 $.toJSON
56         };
58     // Break if no JSON support was found
59     if(!("parse" in JSON) || !("stringify" in JSON)){
60         throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page");
61     }
63     var
64         /* This is the object, that holds the cached values */
65         _storage = {__jstorage_meta:{CRC32:{}}},
67         /* Actual browser storage (localStorage or globalStorage["domain"]) */
68         _storage_service = {jStorage:"{}"},
70         /* DOM element for older IE versions, holds userData behavior */
71         _storage_elm = null,
73         /* How much space does the storage take */
74         _storage_size = 0,
76         /* which backend is currently used */
77         _backend = false,
79         /* onchange observers */
80         _observers = {},
82         /* timeout to wait after onchange event */
83         _observer_timeout = false,
85         /* last update time */
86         _observer_update = 0,
88         /* pubsub observers */
89         _pubsub_observers = {},
91         /* skip published items older than current timestamp */
92         _pubsub_last = +new Date(),
94         /* Next check for TTL */
95         _ttl_timeout,
97         /**
98          * XML encoding and decoding as XML nodes can't be JSON'ized
99          * XML nodes are encoded and decoded if the node is the value to be saved
100          * but not if it's as a property of another object
101          * Eg. -
102          *   $.jStorage.set("key", xmlNode);        // IS OK
103          *   $.jStorage.set("key", {xml: xmlNode}); // NOT OK
104          */
105         _XMLService = {
107             /**
108              * Validates a XML node to be XML
109              * based on jQuery.isXML function
110              */
111             isXML: function(elm){
112                 var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
113                 return documentElement ? documentElement.nodeName !== "HTML" : false;
114             },
116             /**
117              * Encodes a XML node to string
118              * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
119              */
120             encode: function(xmlNode) {
121                 if(!this.isXML(xmlNode)){
122                     return false;
123                 }
124                 try{ // Mozilla, Webkit, Opera
125                     return new XMLSerializer().serializeToString(xmlNode);
126                 }catch(E1) {
127                     try {  // IE
128                         return xmlNode.xml;
129                     }catch(E2){}
130                 }
131                 return false;
132             },
134             /**
135              * Decodes a XML node from string
136              * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
137              */
138             decode: function(xmlString){
139                 var dom_parser = ("DOMParser" in window && (new DOMParser()).parseFromString) ||
140                         (window.ActiveXObject && function(_xmlString) {
141                     var xml_doc = new ActiveXObject("Microsoft.XMLDOM");
142                     xml_doc.async = "false";
143                     xml_doc.loadXML(_xmlString);
144                     return xml_doc;
145                 }),
146                 resultXML;
147                 if(!dom_parser){
148                     return false;
149                 }
150                 resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, "text/xml");
151                 return this.isXML(resultXML)?resultXML:false;
152             }
153         };
156     ////////////////////////// PRIVATE METHODS ////////////////////////
158     /**
159      * Initialization function. Detects if the browser supports DOM Storage
160      * or userData behavior and behaves accordingly.
161      */
162     function _init(){
163         /* Check if browser supports localStorage */
164         var localStorageReallyWorks = false;
165         if("localStorage" in window){
166             try {
167                 window.localStorage.setItem("_tmptest", "tmpval");
168                 localStorageReallyWorks = true;
169                 window.localStorage.removeItem("_tmptest");
170             } catch(BogusQuotaExceededErrorOnIos5) {
171                 // Thanks be to iOS5 Private Browsing mode which throws
172                 // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
173             }
174         }
176         if(localStorageReallyWorks){
177             try {
178                 if(window.localStorage) {
179                     _storage_service = window.localStorage;
180                     _backend = "localStorage";
181                     _observer_update = _storage_service.jStorage_update;
182                 }
183             } catch(E3) {/* Firefox fails when touching localStorage and cookies are disabled */}
184         }
185         /* Check if browser supports globalStorage */
186         else if("globalStorage" in window){
187             try {
188                 if(window.globalStorage) {
189                     if(window.location.hostname == "localhost"){
190                         _storage_service = window.globalStorage["localhost.localdomain"];
191                     }
192                     else{
193                         _storage_service = window.globalStorage[window.location.hostname];
194                     }
195                     _backend = "globalStorage";
196                     _observer_update = _storage_service.jStorage_update;
197                 }
198             } catch(E4) {/* Firefox fails when touching localStorage and cookies are disabled */}
199         }
200         /* Check if browser supports userData behavior */
201         else {
202             _storage_elm = document.createElement("link");
203             if(_storage_elm.addBehavior){
205                 /* Use a DOM element to act as userData storage */
206                 _storage_elm.style.behavior = "url(#default#userData)";
208                 /* userData element needs to be inserted into the DOM! */
209                 document.getElementsByTagName("head")[0].appendChild(_storage_elm);
211                 try{
212                     _storage_elm.load("jStorage");
213                 }catch(E){
214                     // try to reset cache
215                     _storage_elm.setAttribute("jStorage", "{}");
216                     _storage_elm.save("jStorage");
217                     _storage_elm.load("jStorage");
218                 }
220                 var data = "{}";
221                 try{
222                     data = _storage_elm.getAttribute("jStorage");
223                 }catch(E5){}
225                 try{
226                     _observer_update = _storage_elm.getAttribute("jStorage_update");
227                 }catch(E6){}
229                 _storage_service.jStorage = data;
230                 _backend = "userDataBehavior";
231             }else{
232                 _storage_elm = null;
233                 return;
234             }
235         }
237         // Load data from storage
238         _load_storage();
240         // remove dead keys
241         _handleTTL();
243         // start listening for changes
244         _setupObserver();
246         // initialize publish-subscribe service
247         _handlePubSub();
249         // handle cached navigation
250         if("addEventListener" in window){
251             window.addEventListener("pageshow", function(event){
252                 if(event.persisted){
253                     _storageObserver();
254                 }
255             }, false);
256         }
257     }
259     /**
260      * Reload data from storage when needed
261      */
262     function _reloadData(){
263         var data = "{}";
265         if(_backend == "userDataBehavior"){
266             _storage_elm.load("jStorage");
268             try{
269                 data = _storage_elm.getAttribute("jStorage");
270             }catch(E5){}
272             try{
273                 _observer_update = _storage_elm.getAttribute("jStorage_update");
274             }catch(E6){}
276             _storage_service.jStorage = data;
277         }
279         _load_storage();
281         // remove dead keys
282         _handleTTL();
284         _handlePubSub();
285     }
287     /**
288      * Sets up a storage change observer
289      */
290     function _setupObserver(){
291         if(_backend == "localStorage" || _backend == "globalStorage"){
292             if("addEventListener" in window){
293                 window.addEventListener("storage", _storageObserver, false);
294             }else{
295                 document.attachEvent("onstorage", _storageObserver);
296             }
297         }else if(_backend == "userDataBehavior"){
298             setInterval(_storageObserver, 1000);
299         }
300     }
302     /**
303      * Fired on any kind of data change, needs to check if anything has
304      * really been changed
305      */
306     function _storageObserver(){
307         var updateTime;
308         // cumulate change notifications with timeout
309         clearTimeout(_observer_timeout);
310         _observer_timeout = setTimeout(function(){
312             if(_backend == "localStorage" || _backend == "globalStorage"){
313                 updateTime = _storage_service.jStorage_update;
314             }else if(_backend == "userDataBehavior"){
315                 _storage_elm.load("jStorage");
316                 try{
317                     updateTime = _storage_elm.getAttribute("jStorage_update");
318                 }catch(E5){}
319             }
321             if(updateTime && updateTime != _observer_update){
322                 _observer_update = updateTime;
323                 _checkUpdatedKeys();
324             }
326         }, 25);
327     }
329     /**
330      * Reloads the data and checks if any keys are changed
331      */
332     function _checkUpdatedKeys(){
333         var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
334             newCrc32List;
336         _reloadData();
337         newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
339         var key,
340             updated = [],
341             removed = [];
343         for(key in oldCrc32List){
344             if(oldCrc32List.hasOwnProperty(key)){
345                 if(!newCrc32List[key]){
346                     removed.push(key);
347                     continue;
348                 }
349                 if(oldCrc32List[key] != newCrc32List[key] && String(oldCrc32List[key]).substr(0,2) == "2."){
350                     updated.push(key);
351                 }
352             }
353         }
355         for(key in newCrc32List){
356             if(newCrc32List.hasOwnProperty(key)){
357                 if(!oldCrc32List[key]){
358                     updated.push(key);
359                 }
360             }
361         }
363         _fireObservers(updated, "updated");
364         _fireObservers(removed, "deleted");
365     }
367     /**
368      * Fires observers for updated keys
369      *
370      * @param {Array|String} keys Array of key names or a key
371      * @param {String} action What happened with the value (updated, deleted, flushed)
372      */
373     function _fireObservers(keys, action){
374         keys = [].concat(keys || []);
375         if(action == "flushed"){
376             keys = [];
377             for(var key in _observers){
378                 if(_observers.hasOwnProperty(key)){
379                     keys.push(key);
380                 }
381             }
382             action = "deleted";
383         }
384         for(var i=0, len = keys.length; i<len; i++){
385             if(_observers[keys[i]]){
386                 for(var j=0, jlen = _observers[keys[i]].length; j<jlen; j++){
387                     _observers[keys[i]][j](keys[i], action);
388                 }
389             }
390             if(_observers["*"]){
391                 for(var j=0, jlen = _observers["*"].length; j<jlen; j++){
392                     _observers["*"][j](keys[i], action);
393                 }
394             }
395         }
396     }
398     /**
399      * Publishes key change to listeners
400      */
401     function _publishChange(){
402         var updateTime = (+new Date()).toString();
404         if(_backend == "localStorage" || _backend == "globalStorage"){
405             try {
406                 _storage_service.jStorage_update = updateTime;
407             } catch (E8) {
408                 // safari private mode has been enabled after the jStorage initialization
409                 _backend = false;
410             }
411         }else if(_backend == "userDataBehavior"){
412             _storage_elm.setAttribute("jStorage_update", updateTime);
413             _storage_elm.save("jStorage");
414         }
416         _storageObserver();
417     }
419     /**
420      * Loads the data from the storage based on the supported mechanism
421      */
422     function _load_storage(){
423         /* if jStorage string is retrieved, then decode it */
424         if(_storage_service.jStorage){
425             try{
426                 _storage = JSON.parse(String(_storage_service.jStorage));
427             }catch(E6){_storage_service.jStorage = "{}";}
428         }else{
429             _storage_service.jStorage = "{}";
430         }
431         _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
433         if(!_storage.__jstorage_meta){
434             _storage.__jstorage_meta = {};
435         }
436         if(!_storage.__jstorage_meta.CRC32){
437             _storage.__jstorage_meta.CRC32 = {};
438         }
439     }
441     /**
442      * This functions provides the "save" mechanism to store the jStorage object
443      */
444     function _save(){
445         _dropOldEvents(); // remove expired events
446         try{
447             _storage_service.jStorage = JSON.stringify(_storage);
448             // If userData is used as the storage engine, additional
449             if(_storage_elm) {
450                 _storage_elm.setAttribute("jStorage",_storage_service.jStorage);
451                 _storage_elm.save("jStorage");
452             }
453             _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
454         }catch(E7){/* probably cache is full, nothing is saved this way*/}
455     }
457     /**
458      * Function checks if a key is set and is string or numberic
459      *
460      * @param {String} key Key name
461      */
462     function _checkKey(key){
463         if(typeof key != "string" && typeof key != "number"){
464             throw new TypeError("Key name must be string or numeric");
465         }
466         if(key == "__jstorage_meta"){
467             throw new TypeError("Reserved key name");
468         }
469         return true;
470     }
472     /**
473      * Removes expired keys
474      */
475     function _handleTTL(){
476         var curtime, i, TTL, CRC32, nextExpire = Infinity, changed = false, deleted = [];
478         clearTimeout(_ttl_timeout);
480         if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object"){
481             // nothing to do here
482             return;
483         }
485         curtime = +new Date();
486         TTL = _storage.__jstorage_meta.TTL;
488         CRC32 = _storage.__jstorage_meta.CRC32;
489         for(i in TTL){
490             if(TTL.hasOwnProperty(i)){
491                 if(TTL[i] <= curtime){
492                     delete TTL[i];
493                     delete CRC32[i];
494                     delete _storage[i];
495                     changed = true;
496                     deleted.push(i);
497                 }else if(TTL[i] < nextExpire){
498                     nextExpire = TTL[i];
499                 }
500             }
501         }
503         // set next check
504         if(nextExpire != Infinity){
505             _ttl_timeout = setTimeout(_handleTTL, nextExpire - curtime);
506         }
508         // save changes
509         if(changed){
510             _save();
511             _publishChange();
512             _fireObservers(deleted, "deleted");
513         }
514     }
516     /**
517      * Checks if there's any events on hold to be fired to listeners
518      */
519     function _handlePubSub(){
520         var i, len;
521         if(!_storage.__jstorage_meta.PubSub){
522             return;
523         }
524         var pubelm,
525             _pubsubCurrent = _pubsub_last;
527         for(i=len=_storage.__jstorage_meta.PubSub.length-1; i>=0; i--){
528             pubelm = _storage.__jstorage_meta.PubSub[i];
529             if(pubelm[0] > _pubsub_last){
530                 _pubsubCurrent = pubelm[0];
531                 _fireSubscribers(pubelm[1], pubelm[2]);
532             }
533         }
535         _pubsub_last = _pubsubCurrent;
536     }
538     /**
539      * Fires all subscriber listeners for a pubsub channel
540      *
541      * @param {String} channel Channel name
542      * @param {Mixed} payload Payload data to deliver
543      */
544     function _fireSubscribers(channel, payload){
545         if(_pubsub_observers[channel]){
546             for(var i=0, len = _pubsub_observers[channel].length; i<len; i++){
547                 // send immutable data that can't be modified by listeners
548                 try{
549                     _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
550                 }catch(E){};
551             }
552         }
553     }
555     /**
556      * Remove old events from the publish stream (at least 2sec old)
557      */
558     function _dropOldEvents(){
559         if(!_storage.__jstorage_meta.PubSub){
560             return;
561         }
563         var retire = +new Date() - 2000;
565         for(var i=0, len = _storage.__jstorage_meta.PubSub.length; i<len; i++){
566             if(_storage.__jstorage_meta.PubSub[i][0] <= retire){
567                 // deleteCount is needed for IE6
568                 _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
569                 break;
570             }
571         }
573         if(!_storage.__jstorage_meta.PubSub.length){
574             delete _storage.__jstorage_meta.PubSub;
575         }
577     }
579     /**
580      * Publish payload to a channel
581      *
582      * @param {String} channel Channel name
583      * @param {Mixed} payload Payload to send to the subscribers
584      */
585     function _publish(channel, payload){
586         if(!_storage.__jstorage_meta){
587             _storage.__jstorage_meta = {};
588         }
589         if(!_storage.__jstorage_meta.PubSub){
590             _storage.__jstorage_meta.PubSub = [];
591         }
593         _storage.__jstorage_meta.PubSub.unshift([+new Date, channel, payload]);
595         _save();
596         _publishChange();
597     }
600     /**
601      * JS Implementation of MurmurHash2
602      *
603      *  SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
604      *
605      * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
606      * @see http://github.com/garycourt/murmurhash-js
607      * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
608      * @see http://sites.google.com/site/murmurhash/
609      *
610      * @param {string} str ASCII only
611      * @param {number} seed Positive integer only
612      * @return {number} 32-bit positive integer hash
613      */
615     function murmurhash2_32_gc(str, seed) {
616         var
617             l = str.length,
618             h = seed ^ l,
619             i = 0,
620             k;
622         while (l >= 4) {
623             k =
624                 ((str.charCodeAt(i) & 0xff)) |
625                 ((str.charCodeAt(++i) & 0xff) << 8) |
626                 ((str.charCodeAt(++i) & 0xff) << 16) |
627                 ((str.charCodeAt(++i) & 0xff) << 24);
629             k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
630             k ^= k >>> 24;
631             k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
633             h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
635             l -= 4;
636             ++i;
637         }
639         switch (l) {
640             case 3: h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
641             case 2: h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
642             case 1: h ^= (str.charCodeAt(i) & 0xff);
643                 h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
644         }
646         h ^= h >>> 13;
647         h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
648         h ^= h >>> 15;
650         return h >>> 0;
651     }
653     ////////////////////////// PUBLIC INTERFACE /////////////////////////
655     $.jStorage = {
656         /* Version number */
657         version: JSTORAGE_VERSION,
659         /**
660          * Sets a key's value.
661          *
662          * @param {String} key Key to set. If this value is not set or not
663          *              a string an exception is raised.
664          * @param {Mixed} value Value to set. This can be any value that is JSON
665          *              compatible (Numbers, Strings, Objects etc.).
666          * @param {Object} [options] - possible options to use
667          * @param {Number} [options.TTL] - optional TTL value
668          * @return {Mixed} the used value
669          */
670         set: function(key, value, options){
671             _checkKey(key);
673             options = options || {};
675             // undefined values are deleted automatically
676             if(typeof value == "undefined"){
677                 this.deleteKey(key);
678                 return value;
679             }
681             if(_XMLService.isXML(value)){
682                 value = {_is_xml:true,xml:_XMLService.encode(value)};
683             }else if(typeof value == "function"){
684                 return undefined; // functions can't be saved!
685             }else if(value && typeof value == "object"){
686                 // clone the object before saving to _storage tree
687                 value = JSON.parse(JSON.stringify(value));
688             }
690             _storage[key] = value;
692             _storage.__jstorage_meta.CRC32[key] = "2." + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
694             this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
696             _fireObservers(key, "updated");
697             return value;
698         },
700         /**
701          * Looks up a key in cache
702          *
703          * @param {String} key - Key to look up.
704          * @param {mixed} def - Default value to return, if key didn't exist.
705          * @return {Mixed} the key value, default value or null
706          */
707         get: function(key, def){
708             _checkKey(key);
709             if(key in _storage){
710                 if(_storage[key] && typeof _storage[key] == "object" && _storage[key]._is_xml) {
711                     return _XMLService.decode(_storage[key].xml);
712                 }else{
713                     return _storage[key];
714                 }
715             }
716             return typeof(def) == "undefined" ? null : def;
717         },
719         /**
720          * Deletes a key from cache.
721          *
722          * @param {String} key - Key to delete.
723          * @return {Boolean} true if key existed or false if it didn't
724          */
725         deleteKey: function(key){
726             _checkKey(key);
727             if(key in _storage){
728                 delete _storage[key];
729                 // remove from TTL list
730                 if(typeof _storage.__jstorage_meta.TTL == "object" &&
731                   key in _storage.__jstorage_meta.TTL){
732                     delete _storage.__jstorage_meta.TTL[key];
733                 }
735                 delete _storage.__jstorage_meta.CRC32[key];
737                 _save();
738                 _publishChange();
739                 _fireObservers(key, "deleted");
740                 return true;
741             }
742             return false;
743         },
745         /**
746          * Sets a TTL for a key, or remove it if ttl value is 0 or below
747          *
748          * @param {String} key - key to set the TTL for
749          * @param {Number} ttl - TTL timeout in milliseconds
750          * @return {Boolean} true if key existed or false if it didn't
751          */
752         setTTL: function(key, ttl){
753             var curtime = +new Date();
754             _checkKey(key);
755             ttl = Number(ttl) || 0;
756             if(key in _storage){
758                 if(!_storage.__jstorage_meta.TTL){
759                     _storage.__jstorage_meta.TTL = {};
760                 }
762                 // Set TTL value for the key
763                 if(ttl>0){
764                     _storage.__jstorage_meta.TTL[key] = curtime + ttl;
765                 }else{
766                     delete _storage.__jstorage_meta.TTL[key];
767                 }
769                 _save();
771                 _handleTTL();
773                 _publishChange();
774                 return true;
775             }
776             return false;
777         },
779         /**
780          * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
781          *
782          * @param {String} key Key to check
783          * @return {Number} Remaining TTL in milliseconds
784          */
785         getTTL: function(key){
786             var curtime = +new Date(), ttl;
787             _checkKey(key);
788             if(key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]){
789                 ttl = _storage.__jstorage_meta.TTL[key] - curtime;
790                 return ttl || 0;
791             }
792             return 0;
793         },
795         /**
796          * Deletes everything in cache.
797          *
798          * @return {Boolean} Always true
799          */
800         flush: function(){
801             _storage = {__jstorage_meta:{CRC32:{}}};
802             _save();
803             _publishChange();
804             _fireObservers(null, "flushed");
805             return true;
806         },
808         /**
809          * Returns a read-only copy of _storage
810          *
811          * @return {Object} Read-only copy of _storage
812         */
813         storageObj: function(){
814             function F() {}
815             F.prototype = _storage;
816             return new F();
817         },
819         /**
820          * Returns an index of all used keys as an array
821          * ["key1", "key2",.."keyN"]
822          *
823          * @return {Array} Used keys
824         */
825         index: function(){
826             var index = [], i;
827             for(i in _storage){
828                 if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){
829                     index.push(i);
830                 }
831             }
832             return index;
833         },
835         /**
836          * How much space in bytes does the storage take?
837          *
838          * @return {Number} Storage size in chars (not the same as in bytes,
839          *                  since some chars may take several bytes)
840          */
841         storageSize: function(){
842             return _storage_size;
843         },
845         /**
846          * Which backend is currently in use?
847          *
848          * @return {String} Backend name
849          */
850         currentBackend: function(){
851             return _backend;
852         },
854         /**
855          * Test if storage is available
856          *
857          * @return {Boolean} True if storage can be used
858          */
859         storageAvailable: function(){
860             return !!_backend;
861         },
863         /**
864          * Register change listeners
865          *
866          * @param {String} key Key name
867          * @param {Function} callback Function to run when the key changes
868          */
869         listenKeyChange: function(key, callback){
870             _checkKey(key);
871             if(!_observers[key]){
872                 _observers[key] = [];
873             }
874             _observers[key].push(callback);
875         },
877         /**
878          * Remove change listeners
879          *
880          * @param {String} key Key name to unregister listeners against
881          * @param {Function} [callback] If set, unregister the callback, if not - unregister all
882          */
883         stopListening: function(key, callback){
884             _checkKey(key);
886             if(!_observers[key]){
887                 return;
888             }
890             if(!callback){
891                 delete _observers[key];
892                 return;
893             }
895             for(var i = _observers[key].length - 1; i>=0; i--){
896                 if(_observers[key][i] == callback){
897                     _observers[key].splice(i,1);
898                 }
899             }
900         },
902         /**
903          * Subscribe to a Publish/Subscribe event stream
904          *
905          * @param {String} channel Channel name
906          * @param {Function} callback Function to run when the something is published to the channel
907          */
908         subscribe: function(channel, callback){
909             channel = (channel || "").toString();
910             if(!channel){
911                 throw new TypeError("Channel not defined");
912             }
913             if(!_pubsub_observers[channel]){
914                 _pubsub_observers[channel] = [];
915             }
916             _pubsub_observers[channel].push(callback);
917         },
919         /**
920          * Publish data to an event stream
921          *
922          * @param {String} channel Channel name
923          * @param {Mixed} payload Payload to deliver
924          */
925         publish: function(channel, payload){
926             channel = (channel || "").toString();
927             if(!channel){
928                 throw new TypeError("Channel not defined");
929             }
931             _publish(channel, payload);
932         },
934         /**
935          * Reloads the data from browser storage
936          */
937         reInit: function(){
938             _reloadData();
939         },
941         /**
942          * Removes reference from global objects and saves it as jStorage
943          *
944          * @param {Boolean} option if needed to save object as simple "jStorage" in windows context
945          */
946          noConflict: function( saveInGlobal ) {
947             delete window.$.jStorage
949             if ( saveInGlobal ) {
950                 window.jStorage = this;
951             }
953             return this;
954          }
955     };
957     // Initialize jStorage
958     _init();
960 })();