Merge "Don't use isset() to check for null"
[mediawiki.git] / resources / lib / jquery / jquery.jStorage.js
blobcc11aed1d97d3a0472925888cba9597acd78609b
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     'use strict';
40     var
41     /* jStorage version */
42         JSTORAGE_VERSION = '0.4.10',
44         /* detect a dollar object or create one if not found */
45         $ = window.jQuery || window.$ || (window.$ = {}),
47         /* check for a JSON handling support */
48         JSON = {
49             parse: window.JSON && (window.JSON.parse || window.JSON.decode) ||
50                 String.prototype.evalJSON && function(str) {
51                     return String(str).evalJSON();
52             } ||
53                 $.parseJSON ||
54                 $.evalJSON,
55             stringify: Object.toJSON ||
56                 window.JSON && (window.JSON.stringify || window.JSON.encode) ||
57                 $.toJSON
58         };
60     // Break if no JSON support was found
61     if (!('parse' in JSON) || !('stringify' in JSON)) {
62         throw new Error('No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page');
63     }
65     var
66     /* This is the object, that holds the cached values */
67         _storage = {
68             __jstorage_meta: {
69                 CRC32: {}
70             }
71         },
73         /* Actual browser storage (localStorage or globalStorage['domain']) */
74         _storage_service = {
75             jStorage: '{}'
76         },
78         /* DOM element for older IE versions, holds userData behavior */
79         _storage_elm = null,
81         /* How much space does the storage take */
82         _storage_size = 0,
84         /* which backend is currently used */
85         _backend = false,
87         /* onchange observers */
88         _observers = {},
90         /* timeout to wait after onchange event */
91         _observer_timeout = false,
93         /* last update time */
94         _observer_update = 0,
96         /* pubsub observers */
97         _pubsub_observers = {},
99         /* skip published items older than current timestamp */
100         _pubsub_last = +new Date(),
102         /* Next check for TTL */
103         _ttl_timeout,
105         /**
106          * XML encoding and decoding as XML nodes can't be JSON'ized
107          * XML nodes are encoded and decoded if the node is the value to be saved
108          * but not if it's as a property of another object
109          * Eg. -
110          *   $.jStorage.set('key', xmlNode);        // IS OK
111          *   $.jStorage.set('key', {xml: xmlNode}); // NOT OK
112          */
113         _XMLService = {
115             /**
116              * Validates a XML node to be XML
117              * based on jQuery.isXML function
118              */
119             isXML: function(elm) {
120                 var documentElement = (elm ? elm.ownerDocument || elm : 0).documentElement;
121                 return documentElement ? documentElement.nodeName !== 'HTML' : false;
122             },
124             /**
125              * Encodes a XML node to string
126              * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
127              */
128             encode: function(xmlNode) {
129                 if (!this.isXML(xmlNode)) {
130                     return false;
131                 }
132                 try { // Mozilla, Webkit, Opera
133                     return new XMLSerializer().serializeToString(xmlNode);
134                 } catch (E1) {
135                     try { // IE
136                         return xmlNode.xml;
137                     } catch (E2) {}
138                 }
139                 return false;
140             },
142             /**
143              * Decodes a XML node from string
144              * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
145              */
146             decode: function(xmlString) {
147                 var dom_parser = ('DOMParser' in window && (new DOMParser()).parseFromString) ||
148                     (window.ActiveXObject && function(_xmlString) {
149                         var xml_doc = new ActiveXObject('Microsoft.XMLDOM');
150                         xml_doc.async = 'false';
151                         xml_doc.loadXML(_xmlString);
152                         return xml_doc;
153                     }),
154                     resultXML;
155                 if (!dom_parser) {
156                     return false;
157                 }
158                 resultXML = dom_parser.call('DOMParser' in window && (new DOMParser()) || window, xmlString, 'text/xml');
159                 return this.isXML(resultXML) ? resultXML : false;
160             }
161         };
164     ////////////////////////// PRIVATE METHODS ////////////////////////
166     /**
167      * Initialization function. Detects if the browser supports DOM Storage
168      * or userData behavior and behaves accordingly.
169      */
170     function _init() {
171         /* Check if browser supports localStorage */
172         var localStorageReallyWorks = false;
173         if ('localStorage' in window) {
174             try {
175                 window.localStorage.setItem('_tmptest', 'tmpval');
176                 localStorageReallyWorks = true;
177                 window.localStorage.removeItem('_tmptest');
178             } catch (BogusQuotaExceededErrorOnIos5) {
179                 // Thanks be to iOS5 Private Browsing mode which throws
180                 // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
181             }
182         }
184         if (localStorageReallyWorks) {
185             try {
186                 if (window.localStorage) {
187                     _storage_service = window.localStorage;
188                     _backend = 'localStorage';
189                     _observer_update = _storage_service.jStorage_update;
190                 }
191             } catch (E3) { /* Firefox fails when touching localStorage and cookies are disabled */ }
192         }
193         /* Check if browser supports globalStorage */
194         else if ('globalStorage' in window) {
195             try {
196                 if (window.globalStorage) {
197                     if (window.location.hostname == 'localhost') {
198                         _storage_service = window.globalStorage['localhost.localdomain'];
199                     } else {
200                         _storage_service = window.globalStorage[window.location.hostname];
201                     }
202                     _backend = 'globalStorage';
203                     _observer_update = _storage_service.jStorage_update;
204                 }
205             } catch (E4) { /* Firefox fails when touching localStorage and cookies are disabled */ }
206         }
207         /* Check if browser supports userData behavior */
208         else {
209             _storage_elm = document.createElement('link');
210             if (_storage_elm.addBehavior) {
212                 /* Use a DOM element to act as userData storage */
213                 _storage_elm.style.behavior = 'url(#default#userData)';
215                 /* userData element needs to be inserted into the DOM! */
216                 document.getElementsByTagName('head')[0].appendChild(_storage_elm);
218                 try {
219                     _storage_elm.load('jStorage');
220                 } catch (E) {
221                     // try to reset cache
222                     _storage_elm.setAttribute('jStorage', '{}');
223                     _storage_elm.save('jStorage');
224                     _storage_elm.load('jStorage');
225                 }
227                 var data = '{}';
228                 try {
229                     data = _storage_elm.getAttribute('jStorage');
230                 } catch (E5) {}
232                 try {
233                     _observer_update = _storage_elm.getAttribute('jStorage_update');
234                 } catch (E6) {}
236                 _storage_service.jStorage = data;
237                 _backend = 'userDataBehavior';
238             } else {
239                 _storage_elm = null;
240                 return;
241             }
242         }
244         // Load data from storage
245         _load_storage();
247         // remove dead keys
248         _handleTTL();
250         // start listening for changes
251         _setupObserver();
253         // initialize publish-subscribe service
254         _handlePubSub();
256         // handle cached navigation
257         if ('addEventListener' in window) {
258             window.addEventListener('pageshow', function(event) {
259                 if (event.persisted) {
260                     _storageObserver();
261                 }
262             }, false);
263         }
264     }
266     /**
267      * Reload data from storage when needed
268      */
269     function _reloadData() {
270         var data = '{}';
272         if (_backend == 'userDataBehavior') {
273             _storage_elm.load('jStorage');
275             try {
276                 data = _storage_elm.getAttribute('jStorage');
277             } catch (E5) {}
279             try {
280                 _observer_update = _storage_elm.getAttribute('jStorage_update');
281             } catch (E6) {}
283             _storage_service.jStorage = data;
284         }
286         _load_storage();
288         // remove dead keys
289         _handleTTL();
291         _handlePubSub();
292     }
294     /**
295      * Sets up a storage change observer
296      */
297     function _setupObserver() {
298         if (_backend == 'localStorage' || _backend == 'globalStorage') {
299             if ('addEventListener' in window) {
300                 window.addEventListener('storage', _storageObserver, false);
301             } else {
302                 document.attachEvent('onstorage', _storageObserver);
303             }
304         } else if (_backend == 'userDataBehavior') {
305             setInterval(_storageObserver, 1000);
306         }
307     }
309     /**
310      * Fired on any kind of data change, needs to check if anything has
311      * really been changed
312      */
313     function _storageObserver() {
314         var updateTime;
315         // cumulate change notifications with timeout
316         clearTimeout(_observer_timeout);
317         _observer_timeout = setTimeout(function() {
319             if (_backend == 'localStorage' || _backend == 'globalStorage') {
320                 updateTime = _storage_service.jStorage_update;
321             } else if (_backend == 'userDataBehavior') {
322                 _storage_elm.load('jStorage');
323                 try {
324                     updateTime = _storage_elm.getAttribute('jStorage_update');
325                 } catch (E5) {}
326             }
328             if (updateTime && updateTime != _observer_update) {
329                 _observer_update = updateTime;
330                 _checkUpdatedKeys();
331             }
333         }, 25);
334     }
336     /**
337      * Reloads the data and checks if any keys are changed
338      */
339     function _checkUpdatedKeys() {
340         var oldCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32)),
341             newCrc32List;
343         _reloadData();
344         newCrc32List = JSON.parse(JSON.stringify(_storage.__jstorage_meta.CRC32));
346         var key,
347             updated = [],
348             removed = [];
350         for (key in oldCrc32List) {
351             if (oldCrc32List.hasOwnProperty(key)) {
352                 if (!newCrc32List[key]) {
353                     removed.push(key);
354                     continue;
355                 }
356                 if (oldCrc32List[key] != newCrc32List[key] && String(oldCrc32List[key]).substr(0, 2) == '2.') {
357                     updated.push(key);
358                 }
359             }
360         }
362         for (key in newCrc32List) {
363             if (newCrc32List.hasOwnProperty(key)) {
364                 if (!oldCrc32List[key]) {
365                     updated.push(key);
366                 }
367             }
368         }
370         _fireObservers(updated, 'updated');
371         _fireObservers(removed, 'deleted');
372     }
374     /**
375      * Fires observers for updated keys
376      *
377      * @param {Array|String} keys Array of key names or a key
378      * @param {String} action What happened with the value (updated, deleted, flushed)
379      */
380     function _fireObservers(keys, action) {
381         keys = [].concat(keys || []);
383         var i, j, len, jlen;
385         if (action == 'flushed') {
386             keys = [];
387             for (var key in _observers) {
388                 if (_observers.hasOwnProperty(key)) {
389                     keys.push(key);
390                 }
391             }
392             action = 'deleted';
393         }
394         for (i = 0, len = keys.length; i < len; i++) {
395             if (_observers[keys[i]]) {
396                 for (j = 0, jlen = _observers[keys[i]].length; j < jlen; j++) {
397                     _observers[keys[i]][j](keys[i], action);
398                 }
399             }
400             if (_observers['*']) {
401                 for (j = 0, jlen = _observers['*'].length; j < jlen; j++) {
402                     _observers['*'][j](keys[i], action);
403                 }
404             }
405         }
406     }
408     /**
409      * Publishes key change to listeners
410      */
411     function _publishChange() {
412         var updateTime = (+new Date()).toString();
414         if (_backend == 'localStorage' || _backend == 'globalStorage') {
415             try {
416                 _storage_service.jStorage_update = updateTime;
417             } catch (E8) {
418                 // safari private mode has been enabled after the jStorage initialization
419                 _backend = false;
420             }
421         } else if (_backend == 'userDataBehavior') {
422             _storage_elm.setAttribute('jStorage_update', updateTime);
423             _storage_elm.save('jStorage');
424         }
426         _storageObserver();
427     }
429     /**
430      * Loads the data from the storage based on the supported mechanism
431      */
432     function _load_storage() {
433         /* if jStorage string is retrieved, then decode it */
434         if (_storage_service.jStorage) {
435             try {
436                 _storage = JSON.parse(String(_storage_service.jStorage));
437             } catch (E6) {
438                 _storage_service.jStorage = '{}';
439             }
440         } else {
441             _storage_service.jStorage = '{}';
442         }
443         _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
445         if (!_storage.__jstorage_meta) {
446             _storage.__jstorage_meta = {};
447         }
448         if (!_storage.__jstorage_meta.CRC32) {
449             _storage.__jstorage_meta.CRC32 = {};
450         }
451     }
453     /**
454      * This functions provides the 'save' mechanism to store the jStorage object
455      */
456     function _save() {
457         _dropOldEvents(); // remove expired events
458         try {
459             _storage_service.jStorage = JSON.stringify(_storage);
460             // If userData is used as the storage engine, additional
461             if (_storage_elm) {
462                 _storage_elm.setAttribute('jStorage', _storage_service.jStorage);
463                 _storage_elm.save('jStorage');
464             }
465             _storage_size = _storage_service.jStorage ? String(_storage_service.jStorage).length : 0;
466         } catch (E7) { /* probably cache is full, nothing is saved this way*/ }
467     }
469     /**
470      * Function checks if a key is set and is string or numberic
471      *
472      * @param {String} key Key name
473      */
474     function _checkKey(key) {
475         if (typeof key != 'string' && typeof key != 'number') {
476             throw new TypeError('Key name must be string or numeric');
477         }
478         if (key == '__jstorage_meta') {
479             throw new TypeError('Reserved key name');
480         }
481         return true;
482     }
484     /**
485      * Removes expired keys
486      */
487     function _handleTTL() {
488         var curtime, i, TTL, CRC32, nextExpire = Infinity,
489             changed = false,
490             deleted = [];
492         clearTimeout(_ttl_timeout);
494         if (!_storage.__jstorage_meta || typeof _storage.__jstorage_meta.TTL != 'object') {
495             // nothing to do here
496             return;
497         }
499         curtime = +new Date();
500         TTL = _storage.__jstorage_meta.TTL;
502         CRC32 = _storage.__jstorage_meta.CRC32;
503         for (i in TTL) {
504             if (TTL.hasOwnProperty(i)) {
505                 if (TTL[i] <= curtime) {
506                     delete TTL[i];
507                     delete CRC32[i];
508                     delete _storage[i];
509                     changed = true;
510                     deleted.push(i);
511                 } else if (TTL[i] < nextExpire) {
512                     nextExpire = TTL[i];
513                 }
514             }
515         }
517         // set next check
518         if (nextExpire != Infinity) {
519             _ttl_timeout = setTimeout(_handleTTL, Math.min(nextExpire - curtime, 0x7FFFFFFF));
520         }
522         // save changes
523         if (changed) {
524             _save();
525             _publishChange();
526             _fireObservers(deleted, 'deleted');
527         }
528     }
530     /**
531      * Checks if there's any events on hold to be fired to listeners
532      */
533     function _handlePubSub() {
534         var i, len;
535         if (!_storage.__jstorage_meta.PubSub) {
536             return;
537         }
538         var pubelm,
539             _pubsubCurrent = _pubsub_last;
541         for (i = len = _storage.__jstorage_meta.PubSub.length - 1; i >= 0; i--) {
542             pubelm = _storage.__jstorage_meta.PubSub[i];
543             if (pubelm[0] > _pubsub_last) {
544                 _pubsubCurrent = pubelm[0];
545                 _fireSubscribers(pubelm[1], pubelm[2]);
546             }
547         }
549         _pubsub_last = _pubsubCurrent;
550     }
552     /**
553      * Fires all subscriber listeners for a pubsub channel
554      *
555      * @param {String} channel Channel name
556      * @param {Mixed} payload Payload data to deliver
557      */
558     function _fireSubscribers(channel, payload) {
559         if (_pubsub_observers[channel]) {
560             for (var i = 0, len = _pubsub_observers[channel].length; i < len; i++) {
561                 // send immutable data that can't be modified by listeners
562                 try {
563                     _pubsub_observers[channel][i](channel, JSON.parse(JSON.stringify(payload)));
564                 } catch (E) {}
565             }
566         }
567     }
569     /**
570      * Remove old events from the publish stream (at least 2sec old)
571      */
572     function _dropOldEvents() {
573         if (!_storage.__jstorage_meta.PubSub) {
574             return;
575         }
577         var retire = +new Date() - 2000;
579         for (var i = 0, len = _storage.__jstorage_meta.PubSub.length; i < len; i++) {
580             if (_storage.__jstorage_meta.PubSub[i][0] <= retire) {
581                 // deleteCount is needed for IE6
582                 _storage.__jstorage_meta.PubSub.splice(i, _storage.__jstorage_meta.PubSub.length - i);
583                 break;
584             }
585         }
587         if (!_storage.__jstorage_meta.PubSub.length) {
588             delete _storage.__jstorage_meta.PubSub;
589         }
591     }
593     /**
594      * Publish payload to a channel
595      *
596      * @param {String} channel Channel name
597      * @param {Mixed} payload Payload to send to the subscribers
598      */
599     function _publish(channel, payload) {
600         if (!_storage.__jstorage_meta) {
601             _storage.__jstorage_meta = {};
602         }
603         if (!_storage.__jstorage_meta.PubSub) {
604             _storage.__jstorage_meta.PubSub = [];
605         }
607         _storage.__jstorage_meta.PubSub.unshift([+new Date(), channel, payload]);
609         _save();
610         _publishChange();
611     }
614     /**
615      * JS Implementation of MurmurHash2
616      *
617      *  SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
618      *
619      * @author <a href='mailto:gary.court@gmail.com'>Gary Court</a>
620      * @see http://github.com/garycourt/murmurhash-js
621      * @author <a href='mailto:aappleby@gmail.com'>Austin Appleby</a>
622      * @see http://sites.google.com/site/murmurhash/
623      *
624      * @param {string} str ASCII only
625      * @param {number} seed Positive integer only
626      * @return {number} 32-bit positive integer hash
627      */
629     function murmurhash2_32_gc(str, seed) {
630         var
631             l = str.length,
632             h = seed ^ l,
633             i = 0,
634             k;
636         while (l >= 4) {
637             k =
638                 ((str.charCodeAt(i) & 0xff)) |
639                 ((str.charCodeAt(++i) & 0xff) << 8) |
640                 ((str.charCodeAt(++i) & 0xff) << 16) |
641                 ((str.charCodeAt(++i) & 0xff) << 24);
643             k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
644             k ^= k >>> 24;
645             k = (((k & 0xffff) * 0x5bd1e995) + ((((k >>> 16) * 0x5bd1e995) & 0xffff) << 16));
647             h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k;
649             l -= 4;
650             ++i;
651         }
653         switch (l) {
654             case 3:
655                 h ^= (str.charCodeAt(i + 2) & 0xff) << 16;
656             case 2:
657                 h ^= (str.charCodeAt(i + 1) & 0xff) << 8;
658             case 1:
659                 h ^= (str.charCodeAt(i) & 0xff);
660                 h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
661         }
663         h ^= h >>> 13;
664         h = (((h & 0xffff) * 0x5bd1e995) + ((((h >>> 16) * 0x5bd1e995) & 0xffff) << 16));
665         h ^= h >>> 15;
667         return h >>> 0;
668     }
670     ////////////////////////// PUBLIC INTERFACE /////////////////////////
672     $.jStorage = {
673         /* Version number */
674         version: JSTORAGE_VERSION,
676         /**
677          * Sets a key's value.
678          *
679          * @param {String} key Key to set. If this value is not set or not
680          *              a string an exception is raised.
681          * @param {Mixed} value Value to set. This can be any value that is JSON
682          *              compatible (Numbers, Strings, Objects etc.).
683          * @param {Object} [options] - possible options to use
684          * @param {Number} [options.TTL] - optional TTL value, in milliseconds
685          * @return {Mixed} the used value
686          */
687         set: function(key, value, options) {
688             _checkKey(key);
690             options = options || {};
692             // undefined values are deleted automatically
693             if (typeof value == 'undefined') {
694                 this.deleteKey(key);
695                 return value;
696             }
698             if (_XMLService.isXML(value)) {
699                 value = {
700                     _is_xml: true,
701                     xml: _XMLService.encode(value)
702                 };
703             } else if (typeof value == 'function') {
704                 return undefined; // functions can't be saved!
705             } else if (value && typeof value == 'object') {
706                 // clone the object before saving to _storage tree
707                 value = JSON.parse(JSON.stringify(value));
708             }
710             _storage[key] = value;
712             _storage.__jstorage_meta.CRC32[key] = '2.' + murmurhash2_32_gc(JSON.stringify(value), 0x9747b28c);
714             this.setTTL(key, options.TTL || 0); // also handles saving and _publishChange
716             _fireObservers(key, 'updated');
717             return value;
718         },
720         /**
721          * Looks up a key in cache
722          *
723          * @param {String} key - Key to look up.
724          * @param {mixed} def - Default value to return, if key didn't exist.
725          * @return {Mixed} the key value, default value or null
726          */
727         get: function(key, def) {
728             _checkKey(key);
729             if (key in _storage) {
730                 if (_storage[key] && typeof _storage[key] == 'object' && _storage[key]._is_xml) {
731                     return _XMLService.decode(_storage[key].xml);
732                 } else {
733                     return _storage[key];
734                 }
735             }
736             return typeof(def) == 'undefined' ? null : def;
737         },
739         /**
740          * Deletes a key from cache.
741          *
742          * @param {String} key - Key to delete.
743          * @return {Boolean} true if key existed or false if it didn't
744          */
745         deleteKey: function(key) {
746             _checkKey(key);
747             if (key in _storage) {
748                 delete _storage[key];
749                 // remove from TTL list
750                 if (typeof _storage.__jstorage_meta.TTL == 'object' &&
751                     key in _storage.__jstorage_meta.TTL) {
752                     delete _storage.__jstorage_meta.TTL[key];
753                 }
755                 delete _storage.__jstorage_meta.CRC32[key];
757                 _save();
758                 _publishChange();
759                 _fireObservers(key, 'deleted');
760                 return true;
761             }
762             return false;
763         },
765         /**
766          * Sets a TTL for a key, or remove it if ttl value is 0 or below
767          *
768          * @param {String} key - key to set the TTL for
769          * @param {Number} ttl - TTL timeout in milliseconds
770          * @return {Boolean} true if key existed or false if it didn't
771          */
772         setTTL: function(key, ttl) {
773             var curtime = +new Date();
774             _checkKey(key);
775             ttl = Number(ttl) || 0;
776             if (key in _storage) {
778                 if (!_storage.__jstorage_meta.TTL) {
779                     _storage.__jstorage_meta.TTL = {};
780                 }
782                 // Set TTL value for the key
783                 if (ttl > 0) {
784                     _storage.__jstorage_meta.TTL[key] = curtime + ttl;
785                 } else {
786                     delete _storage.__jstorage_meta.TTL[key];
787                 }
789                 _save();
791                 _handleTTL();
793                 _publishChange();
794                 return true;
795             }
796             return false;
797         },
799         /**
800          * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
801          *
802          * @param {String} key Key to check
803          * @return {Number} Remaining TTL in milliseconds
804          */
805         getTTL: function(key) {
806             var curtime = +new Date(),
807                 ttl;
808             _checkKey(key);
809             if (key in _storage && _storage.__jstorage_meta.TTL && _storage.__jstorage_meta.TTL[key]) {
810                 ttl = _storage.__jstorage_meta.TTL[key] - curtime;
811                 return ttl || 0;
812             }
813             return 0;
814         },
816         /**
817          * Deletes everything in cache.
818          *
819          * @return {Boolean} Always true
820          */
821         flush: function() {
822             _storage = {
823                 __jstorage_meta: {
824                     CRC32: {}
825                 }
826             };
827             _save();
828             _publishChange();
829             _fireObservers(null, 'flushed');
830             return true;
831         },
833         /**
834          * Returns a read-only copy of _storage
835          *
836          * @return {Object} Read-only copy of _storage
837          */
838         storageObj: function() {
839             function F() {}
840             F.prototype = _storage;
841             return new F();
842         },
844         /**
845          * Returns an index of all used keys as an array
846          * ['key1', 'key2',..'keyN']
847          *
848          * @return {Array} Used keys
849          */
850         index: function() {
851             var index = [],
852                 i;
853             for (i in _storage) {
854                 if (_storage.hasOwnProperty(i) && i != '__jstorage_meta') {
855                     index.push(i);
856                 }
857             }
858             return index;
859         },
861         /**
862          * How much space in bytes does the storage take?
863          *
864          * @return {Number} Storage size in chars (not the same as in bytes,
865          *                  since some chars may take several bytes)
866          */
867         storageSize: function() {
868             return _storage_size;
869         },
871         /**
872          * Which backend is currently in use?
873          *
874          * @return {String} Backend name
875          */
876         currentBackend: function() {
877             return _backend;
878         },
880         /**
881          * Test if storage is available
882          *
883          * @return {Boolean} True if storage can be used
884          */
885         storageAvailable: function() {
886             return !!_backend;
887         },
889         /**
890          * Register change listeners
891          *
892          * @param {String} key Key name
893          * @param {Function} callback Function to run when the key changes
894          */
895         listenKeyChange: function(key, callback) {
896             _checkKey(key);
897             if (!_observers[key]) {
898                 _observers[key] = [];
899             }
900             _observers[key].push(callback);
901         },
903         /**
904          * Remove change listeners
905          *
906          * @param {String} key Key name to unregister listeners against
907          * @param {Function} [callback] If set, unregister the callback, if not - unregister all
908          */
909         stopListening: function(key, callback) {
910             _checkKey(key);
912             if (!_observers[key]) {
913                 return;
914             }
916             if (!callback) {
917                 delete _observers[key];
918                 return;
919             }
921             for (var i = _observers[key].length - 1; i >= 0; i--) {
922                 if (_observers[key][i] == callback) {
923                     _observers[key].splice(i, 1);
924                 }
925             }
926         },
928         /**
929          * Subscribe to a Publish/Subscribe event stream
930          *
931          * @param {String} channel Channel name
932          * @param {Function} callback Function to run when the something is published to the channel
933          */
934         subscribe: function(channel, callback) {
935             channel = (channel || '').toString();
936             if (!channel) {
937                 throw new TypeError('Channel not defined');
938             }
939             if (!_pubsub_observers[channel]) {
940                 _pubsub_observers[channel] = [];
941             }
942             _pubsub_observers[channel].push(callback);
943         },
945         /**
946          * Publish data to an event stream
947          *
948          * @param {String} channel Channel name
949          * @param {Mixed} payload Payload to deliver
950          */
951         publish: function(channel, payload) {
952             channel = (channel || '').toString();
953             if (!channel) {
954                 throw new TypeError('Channel not defined');
955             }
957             _publish(channel, payload);
958         },
960         /**
961          * Reloads the data from browser storage
962          */
963         reInit: function() {
964             _reloadData();
965         },
967         /**
968          * Removes reference from global objects and saves it as jStorage
969          *
970          * @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
971          */
972         noConflict: function(saveInGlobal) {
973             delete window.$.jStorage;
975             if (saveInGlobal) {
976                 window.jStorage = this;
977             }
979             return this;
980         }
981     };
983     // Initialize jStorage
984     _init();
986 })();