Localisation updates from http://translatewiki.net.
[mediawiki.git] / resources / jquery / jquery.jStorage.js
blob6ca21b5c5354a68d126843b7a214bee510dfe429
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  * Copyright (c) 2010 - 2012 Andris Reinman, andris.reinman@gmail.com
7  * Project homepage: www.jstorage.info
8  *
9  * Licensed under MIT-style license:
10  *
11  * Permission is hereby granted, free of charge, to any person obtaining a copy
12  * of this software and associated documentation files (the "Software"), to deal
13  * in the Software without restriction, including without limitation the rights
14  * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
15  * copies of the Software, and to permit persons to whom the Software is
16  * furnished to do so, subject to the following conditions:
17  *
18  * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
19  * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
20  * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
21  * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
22  * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
23  * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
24  * SOFTWARE.
25  */
27  (function(){
28     var
29         /* jStorage version */
30         JSTORAGE_VERSION = "0.3.0",
32         /* detect a dollar object or create one if not found */
33         $ = window.jQuery || window.$ || (window.$ = {}),
35         /* check for a JSON handling support */
36         JSON = {
37             parse:
38                 window.JSON && (window.JSON.parse || window.JSON.decode) ||
39                 String.prototype.evalJSON && function(str){return String(str).evalJSON();} ||
40                 $.parseJSON ||
41                 $.evalJSON,
42             stringify:
43                 Object.toJSON ||
44                 window.JSON && (window.JSON.stringify || window.JSON.encode) ||
45                 $.toJSON
46         };
48     // Break if no JSON support was found
49     if(!JSON.parse || !JSON.stringify){
50         throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page");
51     }
53     var
54         /* This is the object, that holds the cached values */
55         _storage = {},
57         /* Actual browser storage (localStorage or globalStorage['domain']) */
58         _storage_service = {jStorage:"{}"},
60         /* DOM element for older IE versions, holds userData behavior */
61         _storage_elm = null,
63         /* How much space does the storage take */
64         _storage_size = 0,
66         /* which backend is currently used */
67         _backend = false,
69         /* onchange observers */
70         _observers = {},
72         /* timeout to wait after onchange event */
73         _observer_timeout = false,
75         /* last update time */
76         _observer_update = 0,
78         /* pubsub observers */
79         _pubsub_observers = {},
81         /* skip published items older than current timestamp */
82         _pubsub_last = +new Date(), 
84         /* Next check for TTL */
85         _ttl_timeout,
87         /* crc32 table */
88         _crc32Table = "00000000 77073096 EE0E612C 990951BA 076DC419 706AF48F E963A535 9E6495A3 "+
89              "0EDB8832 79DCB8A4 E0D5E91E 97D2D988 09B64C2B 7EB17CBD E7B82D07 90BF1D91 1DB71064 "+
90              "6AB020F2 F3B97148 84BE41DE 1ADAD47D 6DDDE4EB F4D4B551 83D385C7 136C9856 646BA8C0 "+
91              "FD62F97A 8A65C9EC 14015C4F 63066CD9 FA0F3D63 8D080DF5 3B6E20C8 4C69105E D56041E4 "+
92              "A2677172 3C03E4D1 4B04D447 D20D85FD A50AB56B 35B5A8FA 42B2986C DBBBC9D6 ACBCF940 "+
93              "32D86CE3 45DF5C75 DCD60DCF ABD13D59 26D930AC 51DE003A C8D75180 BFD06116 21B4F4B5 "+
94              "56B3C423 CFBA9599 B8BDA50F 2802B89E 5F058808 C60CD9B2 B10BE924 2F6F7C87 58684C11 "+
95              "C1611DAB B6662D3D 76DC4190 01DB7106 98D220BC EFD5102A 71B18589 06B6B51F 9FBFE4A5 "+
96              "E8B8D433 7807C9A2 0F00F934 9609A88E E10E9818 7F6A0DBB 086D3D2D 91646C97 E6635C01 "+
97              "6B6B51F4 1C6C6162 856530D8 F262004E 6C0695ED 1B01A57B 8208F4C1 F50FC457 65B0D9C6 "+
98              "12B7E950 8BBEB8EA FCB9887C 62DD1DDF 15DA2D49 8CD37CF3 FBD44C65 4DB26158 3AB551CE "+
99              "A3BC0074 D4BB30E2 4ADFA541 3DD895D7 A4D1C46D D3D6F4FB 4369E96A 346ED9FC AD678846 "+
100              "DA60B8D0 44042D73 33031DE5 AA0A4C5F DD0D7CC9 5005713C 270241AA BE0B1010 C90C2086 "+
101              "5768B525 206F85B3 B966D409 CE61E49F 5EDEF90E 29D9C998 B0D09822 C7D7A8B4 59B33D17 "+
102              "2EB40D81 B7BD5C3B C0BA6CAD EDB88320 9ABFB3B6 03B6E20C 74B1D29A EAD54739 9DD277AF "+
103              "04DB2615 73DC1683 E3630B12 94643B84 0D6D6A3E 7A6A5AA8 E40ECF0B 9309FF9D 0A00AE27 "+
104              "7D079EB1 F00F9344 8708A3D2 1E01F268 6906C2FE F762575D 806567CB 196C3671 6E6B06E7 "+
105              "FED41B76 89D32BE0 10DA7A5A 67DD4ACC F9B9DF6F 8EBEEFF9 17B7BE43 60B08ED5 D6D6A3E8 "+
106              "A1D1937E 38D8C2C4 4FDFF252 D1BB67F1 A6BC5767 3FB506DD 48B2364B D80D2BDA AF0A1B4C "+
107              "36034AF6 41047A60 DF60EFC3 A867DF55 316E8EEF 4669BE79 CB61B38C BC66831A 256FD2A0 "+
108              "5268E236 CC0C7795 BB0B4703 220216B9 5505262F C5BA3BBE B2BD0B28 2BB45A92 5CB36A04 "+
109              "C2D7FFA7 B5D0CF31 2CD99E8B 5BDEAE1D 9B64C2B0 EC63F226 756AA39C 026D930A 9C0906A9 "+
110              "EB0E363F 72076785 05005713 95BF4A82 E2B87A14 7BB12BAE 0CB61B38 92D28E9B E5D5BE0D "+
111              "7CDCEFB7 0BDBDF21 86D3D2D4 F1D4E242 68DDB3F8 1FDA836E 81BE16CD F6B9265B 6FB077E1 "+
112              "18B74777 88085AE6 FF0F6A70 66063BCA 11010B5C 8F659EFF F862AE69 616BFFD3 166CCF45 "+
113              "A00AE278 D70DD2EE 4E048354 3903B3C2 A7672661 D06016F7 4969474D 3E6E77DB AED16A4A "+
114              "D9D65ADC 40DF0B66 37D83BF0 A9BCAE53 DEBB9EC5 47B2CF7F 30B5FFE9 BDBDF21C CABAC28A "+
115              "53B39330 24B4A3A6 BAD03605 CDD70693 54DE5729 23D967BF B3667A2E C4614AB8 5D681B02 "+
116              "2A6F2B94 B40BBE37 C30C8EA1 5A05DF1B 2D02EF8D",
118         /**
119          * XML encoding and decoding as XML nodes can't be JSON'ized
120          * XML nodes are encoded and decoded if the node is the value to be saved
121          * but not if it's as a property of another object
122          * Eg. -
123          *   $.jStorage.set("key", xmlNode);        // IS OK
124          *   $.jStorage.set("key", {xml: xmlNode}); // NOT OK
125          */
126         _XMLService = {
128             /**
129              * Validates a XML node to be XML
130              * based on jQuery.isXML function
131              */
132             isXML: function(elm){
133                 var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
134                 return documentElement ? documentElement.nodeName !== "HTML" : false;
135             },
137             /**
138              * Encodes a XML node to string
139              * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
140              */
141             encode: function(xmlNode) {
142                 if(!this.isXML(xmlNode)){
143                     return false;
144                 }
145                 try{ // Mozilla, Webkit, Opera
146                     return new XMLSerializer().serializeToString(xmlNode);
147                 }catch(E1) {
148                     try {  // IE
149                         return xmlNode.xml;
150                     }catch(E2){}
151                 }
152                 return false;
153             },
155             /**
156              * Decodes a XML node from string
157              * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
158              */
159             decode: function(xmlString){
160                 var dom_parser = ("DOMParser" in window && (new DOMParser()).parseFromString) ||
161                         (window.ActiveXObject && function(_xmlString) {
162                     var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
163                     xml_doc.async = 'false';
164                     xml_doc.loadXML(_xmlString);
165                     return xml_doc;
166                 }),
167                 resultXML;
168                 if(!dom_parser){
169                     return false;
170                 }
171                 resultXML = dom_parser.call("DOMParser" in window && (new DOMParser()) || window, xmlString, 'text/xml');
172                 return this.isXML(resultXML)?resultXML:false;
173             }
174         },
176         _localStoragePolyfillSetKey = function(){};
179     ////////////////////////// PRIVATE METHODS ////////////////////////
181     /**
182      * Initialization function. Detects if the browser supports DOM Storage
183      * or userData behavior and behaves accordingly.
184      */
185     function _init(){
186         /* Check if browser supports localStorage */
187         var localStorageReallyWorks = false;
188         if("localStorage" in window){
189             try {
190                 window.localStorage.setItem('_tmptest', 'tmpval');
191                 localStorageReallyWorks = true;
192                 window.localStorage.removeItem('_tmptest');
193             } catch(BogusQuotaExceededErrorOnIos5) {
194                 // Thanks be to iOS5 Private Browsing mode which throws
195                 // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
196             }
197         }
199         if(localStorageReallyWorks){
200             try {
201                 if(window.localStorage) {
202                     _storage_service = window.localStorage;
203                     _backend = "localStorage";
204                     _observer_update = _storage_service.jStorage_update;
205                 }
206             } catch(E3) {/* Firefox fails when touching localStorage and cookies are disabled */}
207         }
208         /* Check if browser supports globalStorage */
209         else if("globalStorage" in window){
210             try {
211                 if(window.globalStorage) {
212                     _storage_service = window.globalStorage[window.location.hostname];
213                     _backend = "globalStorage";
214                     _observer_update = _storage_service.jStorage_update;
215                 }
216             } catch(E4) {/* Firefox fails when touching localStorage and cookies are disabled */}
217         }
218         /* Check if browser supports userData behavior */
219         else {
220             _storage_elm = document.createElement('link');
221             if(_storage_elm.addBehavior){
223                 /* Use a DOM element to act as userData storage */
224                 _storage_elm.style.behavior = 'url(#default#userData)';
226                 /* userData element needs to be inserted into the DOM! */
227                 document.getElementsByTagName('head')[0].appendChild(_storage_elm);
229                 try{
230                     _storage_elm.load("jStorage");
231                 }catch(E){
232                     // try to reset cache
233                     _storage_elm.setAttribute("jStorage", "{}");
234                     _storage_elm.save("jStorage");
235                     _storage_elm.load("jStorage");
236                 }
238                 var data = "{}";
239                 try{
240                     data = _storage_elm.getAttribute("jStorage");
241                 }catch(E5){}
243                 try{
244                     _observer_update = _storage_elm.getAttribute("jStorage_update");
245                 }catch(E6){}
247                 _storage_service.jStorage = data;
248                 _backend = "userDataBehavior";
249             }else{
250                 _storage_elm = null;
251                 return;
252             }
253         }
255         // Load data from storage
256         _load_storage();
258         // remove dead keys
259         _handleTTL();
261         // create localStorage and sessionStorage polyfills if needed
262         _createPolyfillStorage("local");
263         _createPolyfillStorage("session");
265         // start listening for changes
266         _setupObserver();
268         // initialize publish-subscribe service
269         _handlePubSub();
271         // handle cached navigation
272         if("addEventListener" in window){
273             window.addEventListener("pageshow", function(event){
274                 if(event.persisted){
275                     _storageObserver();
276                 }
277             }, false);
278         }
279     }
281     /**
282      * Create a polyfill for localStorage (type="local") or sessionStorage (type="session")
283      *
284      * @param {String} type Either "local" or "session"
285      * @param {Boolean} forceCreate If set to true, recreate the polyfill (needed with flush)
286      */
287     function _createPolyfillStorage(type, forceCreate){
288         var _skipSave = false,
289             _length = 0,
290             i, 
291             storage,
292             storage_source = {};
294             var rand = Math.random();
296         if(!forceCreate && typeof window[type+"Storage"] != "undefined"){
297             return;
298         }
300         // Use globalStorage for localStorage if available
301         if(type == "local" && window.globalStorage){
302             localStorage = window.globalStorage[window.location.hostname];
303             return;
304         }
306         // only IE6/7 from this point on 
307         if(_backend != "userDataBehavior"){
308             return;
309         }
311         // Remove existing storage element if available
312         if(forceCreate && window[type+"Storage"] && window[type+"Storage"].parentNode){
313             window[type+"Storage"].parentNode.removeChild(window[type+"Storage"]);
314         }
316         storage = document.createElement("button");
317         document.getElementsByTagName('head')[0].appendChild(storage);
319         if(type == "local"){
320             storage_source = _storage;
321         }else if(type == "session"){
322             _sessionStoragePolyfillUpdate();
323         }
325         for(i in storage_source){
327             if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i != "length" && typeof storage_source[i] != "undefined"){
328                 if(!(i in storage)){
329                     _length++;
330                 }
331                 storage[i] = storage_source[i];
332             }
333         }
334         
335         // Polyfill API
337         /**
338          * Indicates how many keys are stored in the storage
339          */
340         storage.length = _length;
342         /**
343          * Returns the key of the nth stored value
344          * 
345          * @param {Number} n Index position
346          * @return {String} Key name of the nth stored value
347          */
348         storage.key = function(n){
349             var count = 0, i;
350             _sessionStoragePolyfillUpdate();
351             for(i in storage_source){
352                 if(storage_source.hasOwnProperty(i) && i != "__jstorage_meta" && i!="length" && typeof storage_source[i] != "undefined"){
353                     if(count == n){
354                         return i;
355                     }
356                     count++;
357                 }
358             }
359         }
361         /**
362          * Returns the current value associated with the given key
363          *
364          * @param {String} key key name
365          * @return {Mixed} Stored value
366          */
367         storage.getItem = function(key){
368             _sessionStoragePolyfillUpdate();
369             if(type == "session"){
370                 return storage_source[key];
371             }
372             return $.jStorage.get(key);
373         }
375         /**
376          * Sets or updates value for a give key
377          *
378          * @param {String} key Key name to be updated
379          * @param {String} value String value to be stored 
380          */
381         storage.setItem = function(key, value){
382             if(typeof value == "undefined"){
383                 return;
384             }
385             storage[key] = (value || "").toString();
386         }
388         /**
389          * Removes key from the storage
390          *
391          * @param {String} key Key name to be removed
392          */
393         storage.removeItem = function(key){
394             if(type == "local"){
395                 return $.jStorage.deleteKey(key);
396             }
398             storage[key] = undefined;
399             
400             _skipSave = true;
401             if(key in storage){
402                 storage.removeAttribute(key);
403             }
404             _skipSave = false;
405         }
407         /**
408          * Clear storage
409          */
410         storage.clear = function(){
411             if(type == "session"){
412                 window.name = "";
413                 _createPolyfillStorage("session", true);
414                 return;
415             }
416             $.jStorage.flush();
417         }
419         if(type == "local"){
421             _localStoragePolyfillSetKey = function(key, value){
422                 if(key == "length"){
423                     return;
424                 }
425                 _skipSave = true;
426                 if(typeof value == "undefined"){
427                     if(key in storage){
428                         _length--;
429                         storage.removeAttribute(key);
430                     }
431                 }else{
432                     if(!(key in storage)){
433                         _length++;
434                     }
435                     storage[key] = (value || "").toString();
436                 }
437                 storage.length = _length;
438                 _skipSave = false;
439             }
440         }
442         function _sessionStoragePolyfillUpdate(){
443                 if(type != "session"){
444                     return;
445                 }
446                 try{
447                     storage_source = JSON.parse(window.name || "{}");
448                 }catch(E){
449                     storage_source = {};
450                 }
451             }
453         function _sessionStoragePolyfillSave(){
454             if(type != "session"){
455                 return;
456             }
457             window.name = JSON.stringify(storage_source);
458         };
460         storage.attachEvent("onpropertychange", function(e){
461             if(e.propertyName == "length"){
462                 return;
463             }
465             if(_skipSave || e.propertyName == "length"){
466                 return;
467             }
469             if(type == "local"){
470                 if(!(e.propertyName in storage_source) && typeof storage[e.propertyName] != "undefined"){
471                     _length ++;
472                 }
473             }else if(type == "session"){
474                 _sessionStoragePolyfillUpdate();
475                 if(typeof storage[e.propertyName] != "undefined" && !(e.propertyName in storage_source)){
476                     storage_source[e.propertyName] = storage[e.propertyName];
477                     _length++;
478                 }else if(typeof storage[e.propertyName] == "undefined" && e.propertyName in storage_source){
479                     delete storage_source[e.propertyName];
480                     _length--;
481                 }else{
482                     storage_source[e.propertyName] = storage[e.propertyName];
483                 }
485                 _sessionStoragePolyfillSave();
486                 storage.length = _length;
487                 return;
488             }
490             $.jStorage.set(e.propertyName, storage[e.propertyName]);
491             storage.length = _length;
492         });
494         window[type+"Storage"] = storage;
495     }
497     /**
498      * Reload data from storage when needed
499      */
500     function _reloadData(){
501         var data = "{}";
503         if(_backend == "userDataBehavior"){
504             _storage_elm.load("jStorage");
506             try{
507                 data = _storage_elm.getAttribute("jStorage");
508             }catch(E5){}
510             try{
511                 _observer_update = _storage_elm.getAttribute("jStorage_update");
512             }catch(E6){}
514             _storage_service.jStorage = data;
515         }
517         _load_storage();
519         // remove dead keys
520         _handleTTL();
522         _handlePubSub();
523     }
525     /**
526      * Sets up a storage change observer
527      */
528     function _setupObserver(){
529         if(_backend == "localStorage" || _backend == "globalStorage"){
530             if("addEventListener" in window){
531                 window.addEventListener("storage", _storageObserver, false);
532             }else{
533                 document.attachEvent("onstorage", _storageObserver);
534             }
535         }else if(_backend == "userDataBehavior"){
536             setInterval(_storageObserver, 1000);
537         }
538     }
540     /**
541      * Fired on any kind of data change, needs to check if anything has
542      * really been changed
543      */
544     function _storageObserver(){
545         var updateTime;
546         // cumulate change notifications with timeout
547         clearTimeout(_observer_timeout);
548         _observer_timeout = setTimeout(function(){
550             if(_backend == "localStorage" || _backend == "globalStorage"){
551                 updateTime = _storage_service.jStorage_update;
552             }else if(_backend == "userDataBehavior"){
553                 _storage_elm.load("jStorage");
554                 try{
555                     updateTime = _storage_elm.getAttribute("jStorage_update");
556                 }catch(E5){}
557             }
559             if(updateTime && updateTime != _observer_update){
560                 _observer_update = updateTime;
561                 _checkUpdatedKeys();
562             }
564         }, 25);
565     }
567     /**
568      * Reloads the data and checks if any keys are changed
569      */
570     function _checkUpdatedKeys(){
571         var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
572             newCrc32List;
574         _reloadData();
575         newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
577         var key,
578             updated = [],
579             removed = [];
581         for(key in oldCrc32List){
582             if(oldCrc32List.hasOwnProperty(key)){
583                 if(!newCrc32List[key]){
584                     removed.push(key);
585                     continue;
586                 }
587                 if(oldCrc32List[key] != newCrc32List[key]){
588                     updated.push(key);
589                 }
590             }
591         }
593         for(key in newCrc32List){
594             if(newCrc32List.hasOwnProperty(key)){
595                 if(!oldCrc32List[key]){
596                     updated.push(key);
597                 }
598             }
599         }
601         _fireObservers(updated, "updated");
602         _fireObservers(removed, "deleted");
603     }
605     /**
606      * Fires observers for updated keys
607      *
608      * @param {Array|String} keys Array of key names or a key
609      * @param {String} action What happened with the value (updated, deleted, flushed)
610      */
611     function _fireObservers(keys, action){
612         keys = [].concat(keys || []);
613         if(action == "flushed"){
614             keys = [];
615             for(var key in _observers){
616                 if(_observers.hasOwnProperty(key)){
617                     keys.push(key);
618                 }
619             }
620             action = "deleted";
621         }
622         for(var i=0, len = keys.length; i<len; i++){
623             if(_observers[keys[i]]){
624                 for(var j=0, jlen = _observers[keys[i]].length; j<jlen; j++){
625                     _observers[keys[i]][j](keys[i], action);
626                 }
627             }
628         }
629     }
631     /**
632      * Publishes key change to listeners
633      */
634     function _publishChange(){
635         var updateTime = (+new Date()).toString();
637         if(_backend == "localStorage" || _backend == "globalStorage"){
638             _storage_service.jStorage_update = updateTime;
639         }else if(_backend == "userDataBehavior"){
640             _storage_elm.setAttribute("jStorage_update", updateTime);
641             _storage_elm.save("jStorage");
642         }
644         _storageObserver();
645     }
647     /**
648      * Loads the data from the storage based on the supported mechanism
649      */
650     function _load_storage(){
651         /* if jStorage string is retrieved, then decode it */
652         if(_storage_service.jStorage){
653             try{
654                 _storage = JSON.parse(String(_storage_service.jStorage));
655             }catch(E6){_storage_service.jStorage = "{}";}
656         }else{
657             _storage_service.jStorage = "{}";
658         }
659         _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
661         if(!_storage.__jstorage_meta){
662             _storage.__jstorage_meta = {};
663         }
664         if(!_storage.__jstorage_meta.CRC32){
665             _storage.__jstorage_meta.CRC32 = {};
666         }
667     }
669     /**
670      * This functions provides the "save" mechanism to store the jStorage object
671      */
672     function _save(){
673         _dropOldEvents(); // remove expired events
674         try{
675             _storage_service.jStorage = JSON.stringify(_storage);
676             // If userData is used as the storage engine, additional
677             if(_storage_elm) {
678                 _storage_elm.setAttribute("jStorage",_storage_service.jStorage);
679                 _storage_elm.save("jStorage");
680             }
681             _storage_size = _storage_service.jStorage?String(_storage_service.jStorage).length:0;
682         }catch(E7){/* probably cache is full, nothing is saved this way*/}
683     }
685     /**
686      * Function checks if a key is set and is string or numberic
687      *
688      * @param {String} key Key name
689      */
690     function _checkKey(key){
691         if(!key || (typeof key != "string" && typeof key != "number")){
692             throw new TypeError('Key name must be string or numeric');
693         }
694         if(key == "__jstorage_meta"){
695             throw new TypeError('Reserved key name');
696         }
697         return true;
698     }
700     /**
701      * Removes expired keys
702      */
703     function _handleTTL(){
704         var curtime, i, TTL, CRC32, nextExpire = Infinity, changed = false, deleted = [];
706         clearTimeout(_ttl_timeout);
708         if(!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != "object"){
709             // nothing to do here
710             return;
711         }
713         curtime = +new Date();
714         TTL = _storage.__jstorage_meta.TTL;
716         CRC32 = _storage.__jstorage_meta.CRC32;
717         for(i in TTL){
718             if(TTL.hasOwnProperty(i)){
719                 if(TTL[i] <= curtime){
720                     delete TTL[i];
721                     delete CRC32[i];
722                     delete _storage[i];
723                     changed = true;
724                     deleted.push(i);
725                 }else if(TTL[i] < nextExpire){
726                     nextExpire = TTL[i];
727                 }
728             }
729         }
731         // set next check
732         if(nextExpire != Infinity){
733             _ttl_timeout = setTimeout(_handleTTL, nextExpire - curtime);
734         }
736         // save changes
737         if(changed){
738             _save();
739             _publishChange();
740             _fireObservers(deleted, "deleted");
741         }
742     }
744     /**
745      * Checks if there's any events on hold to be fired to listeners
746      */
747     function _handlePubSub(){
748         if(!_storage.__jstorage_meta.PubSub){
749             return;
750         }
751         var pubelm,
752             _pubsubCurrent = _pubsub_last;
754         for(var i=len=_storage.__jstorage_meta.PubSub.length-1; i>=0; i--){
755             pubelm = _storage.__jstorage_meta.PubSub[i];
756             if(pubelm[0] > _pubsub_last){
757                 _pubsubCurrent = pubelm[0];
758                 _fireSubscribers(pubelm[1], pubelm[2]);
759             }
760         }
762         _pubsub_last = _pubsubCurrent;
763     }
765     /**
766      * Fires all subscriber listeners for a pubsub channel
767      *
768      * @param {String} channel Channel name
769      * @param {Mixed} payload Payload data to deliver
770      */
771     function _fireSubscribers(channel, payload){
772         if(_pubsub_observers[channel]){
773             for(var i=0, len = _pubsub_observers[channel].length; i<len; i++){
774                 // send immutable data that can't be modified by listeners
775                 _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
776             }
777         }
778     }
780     /**
781      * Remove old events from the publish stream (at least 2sec old)
782      */
783     function _dropOldEvents(){
784         if(!_storage.__jstorage_meta.PubSub){
785             return;
786         }
788         var retire = +new Date() - 2000;
790         for(var i=0, len = _storage.__jstorage_meta.PubSub.length; i<len; i++){
791             if(_storage.__jstorage_meta.PubSub[i][0] <= retire){
792                 // deleteCount is needed for IE6
793                 _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
794                 break;
795             }
796         }
798         if(!_storage.__jstorage_meta.PubSub.length){
799             delete _storage.__jstorage_meta.PubSub;
800         }
802     }
804     /**
805      * Publish payload to a channel
806      *
807      * @param {String} channel Channel name
808      * @param {Mixed} payload Payload to send to the subscribers
809      */
810     function _publish(channel, payload){
811         if(!_storage.__jstorage_meta){
812             _storage.__jstorage_meta = {};
813         }
814         if(!_storage.__jstorage_meta.PubSub){
815             _storage.__jstorage_meta.PubSub = [];
816         }
817         
818         _storage.__jstorage_meta.PubSub.unshift([+new Date, channel, payload]);
820         _save();
821         _publishChange();
822     }
824     /**
825      * CRC32 calculation based on http://noteslog.com/post/crc32-for-javascript/
826      *
827      * @param {String} str String to be hashed
828      * @param {Number} [crc] Last crc value in case of streams
829      */
830     function _crc32(str, crc){
831         crc = crc || 0;
833         var n = 0, //a number between 0 and 255
834             x = 0; //an hex number
836         crc = crc ^ (-1);
837         for(var i = 0, len = str.length; i < len; i++){
838             n = (crc ^ str.charCodeAt(i)) & 0xFF;
839             x = "0x" + _crc32Table.substr(n * 9, 8);
840             crc = (crc >>> 8)^x;
841         }
842         return crc^(-1);
843     }
845     ////////////////////////// PUBLIC INTERFACE /////////////////////////
847     $.jStorage = {
848         /* Version number */
849         version: JSTORAGE_VERSION,
851         /**
852          * Sets a key's value.
853          *
854          * @param {String} key Key to set. If this value is not set or not
855          *              a string an exception is raised.
856          * @param {Mixed} value Value to set. This can be any value that is JSON
857          *              compatible (Numbers, Strings, Objects etc.).
858          * @param {Object} [options] - possible options to use
859          * @param {Number} [options.TTL] - optional TTL value
860          * @return {Mixed} the used value
861          */
862         set: function(key, value, options){
863             _checkKey(key);
865             options = options || {};
867             // undefined values are deleted automatically
868             if(typeof value == "undefined"){
869                 this.deleteKey(key);
870                 return value;
871             }
873             if(_XMLService.isXML(value)){
874                 value = {_is_xml:true,xml:_XMLService.encode(value)};
875             }else if(typeof value == "function"){
876                 return undefined; // functions can't be saved!
877             }else if(value && typeof value == "object"){
878                 // clone the object before saving to _storage tree
879                 value = JSON.parse(JSON.stringify(value));
880             }
882             _storage[key] = value;
884             _storage.__jstorage_meta.CRC32[key] = _crc32(JSON.stringify(value));
886             this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
888             _localStoragePolyfillSetKey(key, value);
890             _fireObservers(key, "updated");
891             return value;
892         },
894         /**
895          * Looks up a key in cache
896          *
897          * @param {String} key - Key to look up.
898          * @param {mixed} def - Default value to return, if key didn't exist.
899          * @return {Mixed} the key value, default value or null
900          */
901         get: function(key, def){
902             _checkKey(key);
903             if(key in _storage){
904                 if(_storage[key] && typeof _storage[key] == "object" &&
905                         _storage[key]._is_xml &&
906                             _storage[key]._is_xml){
907                     return _XMLService.decode(_storage[key].xml);
908                 }else{
909                     return _storage[key];
910                 }
911             }
912             return typeof(def) == 'undefined' ? null : def;
913         },
915         /**
916          * Deletes a key from cache.
917          *
918          * @param {String} key - Key to delete.
919          * @return {Boolean} true if key existed or false if it didn't
920          */
921         deleteKey: function(key){
922             _checkKey(key);
923             if(key in _storage){
924                 delete _storage[key];
925                 // remove from TTL list
926                 if(typeof _storage.__jstorage_meta.TTL == "object" &&
927                   key in _storage.__jstorage_meta.TTL){
928                     delete _storage.__jstorage_meta.TTL[key];
929                 }
931                 delete _storage.__jstorage_meta.CRC32[key];
932                 _localStoragePolyfillSetKey(key, undefined);
934                 _save();
935                 _publishChange();
936                 _fireObservers(key, "deleted");
937                 return true;
938             }
939             return false;
940         },
942         /**
943          * Sets a TTL for a key, or remove it if ttl value is 0 or below
944          *
945          * @param {String} key - key to set the TTL for
946          * @param {Number} ttl - TTL timeout in milliseconds
947          * @return {Boolean} true if key existed or false if it didn't
948          */
949         setTTL: function(key, ttl){
950             var curtime = +new Date();
951             _checkKey(key);
952             ttl = Number(ttl) || 0;
953             if(key in _storage){
955                 if(!_storage.__jstorage_meta.TTL){
956                     _storage.__jstorage_meta.TTL = {};
957                 }
959                 // Set TTL value for the key
960                 if(ttl>0){
961                     _storage.__jstorage_meta.TTL[key] = curtime + ttl;
962                 }else{
963                     delete _storage.__jstorage_meta.TTL[key];
964                 }
966                 _save();
968                 _handleTTL();
970                 _publishChange();
971                 return true;
972             }
973             return false;
974         },
976         /**
977          * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
978          *
979          * @param {String} key Key to check
980          * @return {Number} Remaining TTL in milliseconds
981          */
982         getTTL: function(key){
983             var curtime = +new Date(), ttl;
984             _checkKey(key);
985             if(key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]){
986                 ttl = _storage.__jstorage_meta.TTL[key] - curtime;
987                 return ttl || 0;
988             }
989             return 0;
990         },
992         /**
993          * Deletes everything in cache.
994          *
995          * @return {Boolean} Always true
996          */
997         flush: function(){
998             _storage = {__jstorage_meta:{CRC32:{}}};
999             _createPolyfillStorage("local", true);
1000             _save();
1001             _publishChange();
1002             _fireObservers(null, "flushed");
1003             return true;
1004         },
1006         /**
1007          * Returns a read-only copy of _storage
1008          *
1009          * @return {Object} Read-only copy of _storage
1010         */
1011         storageObj: function(){
1012             function F() {}
1013             F.prototype = _storage;
1014             return new F();
1015         },
1017         /**
1018          * Returns an index of all used keys as an array
1019          * ['key1', 'key2',..'keyN']
1020          *
1021          * @return {Array} Used keys
1022         */
1023         index: function(){
1024             var index = [], i;
1025             for(i in _storage){
1026                 if(_storage.hasOwnProperty(i) && i != "__jstorage_meta"){
1027                     index.push(i);
1028                 }
1029             }
1030             return index;
1031         },
1033         /**
1034          * How much space in bytes does the storage take?
1035          *
1036          * @return {Number} Storage size in chars (not the same as in bytes,
1037          *                  since some chars may take several bytes)
1038          */
1039         storageSize: function(){
1040             return _storage_size;
1041         },
1043         /**
1044          * Which backend is currently in use?
1045          *
1046          * @return {String} Backend name
1047          */
1048         currentBackend: function(){
1049             return _backend;
1050         },
1052         /**
1053          * Test if storage is available
1054          *
1055          * @return {Boolean} True if storage can be used
1056          */
1057         storageAvailable: function(){
1058             return !!_backend;
1059         },
1061         /**
1062          * Register change listeners
1063          *
1064          * @param {String} key Key name
1065          * @param {Function} callback Function to run when the key changes
1066          */
1067         listenKeyChange: function(key, callback){
1068             _checkKey(key);
1069             if(!_observers[key]){
1070                 _observers[key] = [];
1071             }
1072             _observers[key].push(callback);
1073         },
1075         /**
1076          * Remove change listeners
1077          *
1078          * @param {String} key Key name to unregister listeners against
1079          * @param {Function} [callback] If set, unregister the callback, if not - unregister all
1080          */
1081         stopListening: function(key, callback){
1082             _checkKey(key);
1084             if(!_observers[key]){
1085                 return;
1086             }
1088             if(!callback){
1089                 delete _observers[key];
1090                 return;
1091             }
1093             for(var i = _observers[key].length - 1; i>=0; i--){
1094                 if(_observers[key][i] == callback){
1095                     _observers[key].splice(i,1);
1096                 }
1097             }
1098         },
1100         /**
1101          * Subscribe to a Publish/Subscribe event stream
1102          *
1103          * @param {String} channel Channel name
1104          * @param {Function} callback Function to run when the something is published to the channel
1105          */
1106         subscribe: function(channel, callback){
1107             channel = (channel || "").toString();
1108             if(!channel){
1109                 throw new TypeError('Channel not defined');
1110             }
1111             if(!_pubsub_observers[channel]){
1112                 _pubsub_observers[channel] = [];
1113             }
1114             _pubsub_observers[channel].push(callback);
1115         },
1117         /**
1118          * Publish data to an event stream
1119          *
1120          * @param {String} channel Channel name
1121          * @param {Mixed} payload Payload to deliver
1122          */
1123         publish: function(channel, payload){
1124             channel = (channel || "").toString();
1125             if(!channel){
1126                 throw new TypeError('Channel not defined');
1127             }
1129             _publish(channel, payload);
1130         },
1132         /**
1133          * Reloads the data from browser storage
1134          */
1135         reInit: function(){
1136             _reloadData();
1137         }
1138     };
1140     // Initialize jStorage
1141     _init();
1143 })();