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