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+
6 * Author: Andris Reinman, andris.reinman@gmail.com
7 * Project homepage: www.jstorage.info
9 * Licensed under Unlicense:
11 * This is free and unencumbered software released into the public domain.
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
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.
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.
34 * For more information, please refer to <http://unlicense.org/>
37 /* global ActiveXObject: false */
38 /* jshint browser: true */
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 */
52 parse
: window
.JSON
&& (window
.JSON
.parse
|| window
.JSON
.decode
) ||
53 String
.prototype.evalJSON
&& function(str
) {
54 return String(str
).evalJSON();
58 stringify
: Object
.toJSON
||
59 window
.JSON
&& (window
.JSON
.stringify
|| window
.JSON
.encode
) ||
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');
69 /* This is the object, that holds the cached values */
76 /* Actual browser storage (localStorage or globalStorage['domain']) */
81 /* DOM element for older IE versions, holds userData behavior */
84 /* How much space does the storage take */
87 /* which backend is currently used */
90 /* onchange observers */
93 /* timeout to wait after onchange event */
94 _observer_timeout
= false,
96 /* last update time */
99 /* pubsub observers */
100 _pubsub_observers
= {},
102 /* skip published items older than current timestamp */
103 _pubsub_last
= +new Date(),
105 /* Next check for TTL */
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
113 * $.jStorage.set('key', xmlNode); // IS OK
114 * $.jStorage.set('key', {xml: xmlNode}); // NOT OK
119 * Validates a XML node to be XML
120 * based on jQuery.isXML function
122 isXML: function(elm
) {
123 var documentElement
= (elm
? elm
.ownerDocument
|| elm
: 0).documentElement
;
124 return documentElement
? documentElement
.nodeName
!== 'HTML' : false;
128 * Encodes a XML node to string
129 * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
131 encode: function(xmlNode
) {
132 if (!this.isXML(xmlNode
)) {
135 try { // Mozilla, Webkit, Opera
136 return new XMLSerializer().serializeToString(xmlNode
);
146 * Decodes a XML node from string
147 * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
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
);
161 resultXML
= dom_parser
.call('DOMParser' in window
&& (new DOMParser()) || window
, xmlString
, 'text/xml');
162 return this.isXML(resultXML
) ? resultXML
: false;
167 ////////////////////////// PRIVATE METHODS ////////////////////////
170 * Initialization function. Detects if the browser supports DOM Storage
171 * or userData behavior and behaves accordingly.
174 /* Check if browser supports localStorage */
175 var localStorageReallyWorks
= false;
176 if ('localStorage' in window
) {
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.
187 if (localStorageReallyWorks
) {
189 if (window
.localStorage
) {
190 _storage_service
= window
.localStorage
;
191 _backend
= 'localStorage';
192 _observer_update
= _storage_service
.jStorage_update
;
194 } catch (E3
) { /* Firefox fails when touching localStorage and cookies are disabled */ }
196 /* Check if browser supports globalStorage */
197 else if ('globalStorage' in window
) {
199 if (window
.globalStorage
) {
200 if (window
.location
.hostname
== 'localhost') {
201 _storage_service
= window
.globalStorage
['localhost.localdomain'];
203 _storage_service
= window
.globalStorage
[window
.location
.hostname
];
205 _backend
= 'globalStorage';
206 _observer_update
= _storage_service
.jStorage_update
;
208 } catch (E4
) { /* Firefox fails when touching localStorage and cookies are disabled */ }
210 /* Check if browser supports userData behavior */
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
);
222 _storage_elm
.load('jStorage');
224 // try to reset cache
225 _storage_elm
.setAttribute('jStorage', '{}');
226 _storage_elm
.save('jStorage');
227 _storage_elm
.load('jStorage');
232 data
= _storage_elm
.getAttribute('jStorage');
236 _observer_update
= _storage_elm
.getAttribute('jStorage_update');
239 _storage_service
.jStorage
= data
;
240 _backend
= 'userDataBehavior';
247 // Load data from storage
253 // start listening for changes
256 // initialize publish-subscribe service
259 // handle cached navigation
260 if ('addEventListener' in window
) {
261 window
.addEventListener('pageshow', function(event
) {
262 if (event
.persisted
) {
270 * Reload data from storage when needed
272 function _reloadData() {
275 if (_backend
== 'userDataBehavior') {
276 _storage_elm
.load('jStorage');
279 data
= _storage_elm
.getAttribute('jStorage');
283 _observer_update
= _storage_elm
.getAttribute('jStorage_update');
286 _storage_service
.jStorage
= data
;
298 * Sets up a storage change observer
300 function _setupObserver() {
301 if (_backend
== 'localStorage' || _backend
== 'globalStorage') {
302 if ('addEventListener' in window
) {
303 window
.addEventListener('storage', _storageObserver
, false);
305 document
.attachEvent('onstorage', _storageObserver
);
307 } else if (_backend
== 'userDataBehavior') {
308 setInterval(_storageObserver
, 1000);
313 * Fired on any kind of data change, needs to check if anything has
314 * really been changed
316 function _storageObserver() {
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');
327 updateTime
= _storage_elm
.getAttribute('jStorage_update');
331 if (updateTime
&& updateTime
!= _observer_update
) {
332 _observer_update
= updateTime
;
340 * Reloads the data and checks if any keys are changed
342 function _checkUpdatedKeys() {
343 var oldCrc32List
= JSON
.parse(JSON
.stringify(_storage
.__jstorage_meta
.CRC32
)),
347 newCrc32List
= JSON
.parse(JSON
.stringify(_storage
.__jstorage_meta
.CRC32
));
353 for (key
in oldCrc32List
) {
354 if (oldCrc32List
.hasOwnProperty(key
)) {
355 if (!newCrc32List
[key
]) {
359 if (oldCrc32List
[key
] != newCrc32List
[key
] && String(oldCrc32List
[key
]).substr(0, 2) == '2.') {
365 for (key
in newCrc32List
) {
366 if (newCrc32List
.hasOwnProperty(key
)) {
367 if (!oldCrc32List
[key
]) {
373 _fireObservers(updated
, 'updated');
374 _fireObservers(removed
, 'deleted');
378 * Fires observers for updated keys
380 * @param {Array|String} keys Array of key names or a key
381 * @param {String} action What happened with the value (updated, deleted, flushed)
383 function _fireObservers(keys
, action
) {
384 keys
= [].concat(keys
|| []);
388 if (action
== 'flushed') {
390 for (var key
in _observers
) {
391 if (_observers
.hasOwnProperty(key
)) {
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
);
403 if (_observers
['*']) {
404 for (j
= 0, jlen
= _observers
['*'].length
; j
< jlen
; j
++) {
405 _observers
['*'][j
](keys
[i
], action
);
412 * Publishes key change to listeners
414 function _publishChange() {
415 var updateTime
= (+new Date()).toString();
417 if (_backend
== 'localStorage' || _backend
== 'globalStorage') {
419 _storage_service
.jStorage_update
= updateTime
;
421 // safari private mode has been enabled after the jStorage initialization
424 } else if (_backend
== 'userDataBehavior') {
425 _storage_elm
.setAttribute('jStorage_update', updateTime
);
426 _storage_elm
.save('jStorage');
433 * Loads the data from the storage based on the supported mechanism
435 function _load_storage() {
436 /* if jStorage string is retrieved, then decode it */
437 if (_storage_service
.jStorage
) {
439 _storage
= JSON
.parse(String(_storage_service
.jStorage
));
441 _storage_service
.jStorage
= '{}';
444 _storage_service
.jStorage
= '{}';
446 _storage_size
= _storage_service
.jStorage
? String(_storage_service
.jStorage
).length
: 0;
448 if (!_storage
.__jstorage_meta
) {
449 _storage
.__jstorage_meta
= {};
451 if (!_storage
.__jstorage_meta
.CRC32
) {
452 _storage
.__jstorage_meta
.CRC32
= {};
457 * This functions provides the 'save' mechanism to store the jStorage object
460 _dropOldEvents(); // remove expired events
462 _storage_service
.jStorage
= JSON
.stringify(_storage
);
463 // If userData is used as the storage engine, additional
465 _storage_elm
.setAttribute('jStorage', _storage_service
.jStorage
);
466 _storage_elm
.save('jStorage');
468 _storage_size
= _storage_service
.jStorage
? String(_storage_service
.jStorage
).length
: 0;
469 } catch (E7
) { /* probably cache is full, nothing is saved this way*/ }
473 * Function checks if a key is set and is string or numberic
475 * @param {String} key Key name
477 function _checkKey(key
) {
478 if (typeof key
!= 'string' && typeof key
!= 'number') {
479 throw new TypeError('Key name must be string or numeric');
481 if (key
== '__jstorage_meta') {
482 throw new TypeError('Reserved key name');
488 * Removes expired keys
490 function _handleTTL() {
491 var curtime
, i
, TTL
, CRC32
, nextExpire
= Infinity
,
495 clearTimeout(_ttl_timeout
);
497 if (!_storage
.__jstorage_meta
|| typeof _storage
.__jstorage_meta
.TTL
!= 'object') {
498 // nothing to do here
502 curtime
= +new Date();
503 TTL
= _storage
.__jstorage_meta
.TTL
;
505 CRC32
= _storage
.__jstorage_meta
.CRC32
;
507 if (TTL
.hasOwnProperty(i
)) {
508 if (TTL
[i
] <= curtime
) {
514 } else if (TTL
[i
] < nextExpire
) {
521 if (nextExpire
!= Infinity
) {
522 _ttl_timeout
= setTimeout(_handleTTL
, Math
.min(nextExpire
- curtime
, 0x7FFFFFFF));
529 _fireObservers(deleted
, 'deleted');
534 * Checks if there's any events on hold to be fired to listeners
536 function _handlePubSub() {
538 if (!_storage
.__jstorage_meta
.PubSub
) {
542 _pubsubCurrent
= _pubsub_last
,
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
);
553 for (i
= needFired
.length
- 1; i
>= 0; i
--) {
554 _fireSubscribers(needFired
[i
][1], needFired
[i
][2]);
557 _pubsub_last
= _pubsubCurrent
;
561 * Fires all subscriber listeners for a pubsub channel
563 * @param {String} channel Channel name
564 * @param {Mixed} payload Payload data to deliver
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
571 _pubsub_observers
[channel
][i
](channel
, JSON
.parse(JSON
.stringify(payload
)));
578 * Remove old events from the publish stream (at least 2sec old)
580 function _dropOldEvents() {
581 if (!_storage
.__jstorage_meta
.PubSub
) {
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
);
595 if (!_storage
.__jstorage_meta
.PubSub
.length
) {
596 delete _storage
.__jstorage_meta
.PubSub
;
602 * Publish payload to a channel
604 * @param {String} channel Channel name
605 * @param {Mixed} payload Payload to send to the subscribers
607 function _publish(channel
, payload
) {
608 if (!_storage
.__jstorage_meta
) {
609 _storage
.__jstorage_meta
= {};
611 if (!_storage
.__jstorage_meta
.PubSub
) {
612 _storage
.__jstorage_meta
.PubSub
= [];
615 _storage
.__jstorage_meta
.PubSub
.unshift([+new Date(), channel
, payload
]);
623 * JS Implementation of MurmurHash2
625 * SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
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/
632 * @param {string} str ASCII only
633 * @param {number} seed Positive integer only
634 * @return {number} 32-bit positive integer hash
637 function murmurhash2_32_gc(str
, seed
) {
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));
653 k
= (((k
& 0xffff) * 0x5bd1e995) + ((((k
>>> 16) * 0x5bd1e995) & 0xffff) << 16));
655 h
= (((h
& 0xffff) * 0x5bd1e995) + ((((h
>>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k
;
663 h
^= (str
.charCodeAt(i
+ 2) & 0xff) << 16;
666 h
^= (str
.charCodeAt(i
+ 1) & 0xff) << 8;
669 h
^= (str
.charCodeAt(i
) & 0xff);
670 h
= (((h
& 0xffff) * 0x5bd1e995) + ((((h
>>> 16) * 0x5bd1e995) & 0xffff) << 16));
674 h
= (((h
& 0xffff) * 0x5bd1e995) + ((((h
>>> 16) * 0x5bd1e995) & 0xffff) << 16));
680 ////////////////////////// PUBLIC INTERFACE /////////////////////////
684 version
: JSTORAGE_VERSION
,
687 * Sets a key's value.
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
697 set: function(key
, value
, options
) {
700 options
= options
|| {};
702 // undefined values are deleted automatically
703 if (typeof value
== 'undefined') {
708 if (_XMLService
.isXML(value
)) {
711 xml
: _XMLService
.encode(value
)
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
));
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');
731 * Looks up a key in cache
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
737 get: function(key
, def
) {
739 if (key
in _storage
) {
740 if (_storage
[key
] && typeof _storage
[key
] == 'object' && _storage
[key
]._is_xml
) {
741 return _XMLService
.decode(_storage
[key
].xml
);
743 return _storage
[key
];
746 return typeof(def
) == 'undefined' ? null : def
;
750 * Deletes a key from cache.
752 * @param {String} key - Key to delete.
753 * @return {Boolean} true if key existed or false if it didn't
755 deleteKey: function(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
];
765 delete _storage
.__jstorage_meta
.CRC32
[key
];
769 _fireObservers(key
, 'deleted');
776 * Sets a TTL for a key, or remove it if ttl value is 0 or below
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
782 setTTL: function(key
, ttl
) {
783 var curtime
= +new Date();
785 ttl
= Number(ttl
) || 0;
786 if (key
in _storage
) {
788 if (!_storage
.__jstorage_meta
.TTL
) {
789 _storage
.__jstorage_meta
.TTL
= {};
792 // Set TTL value for the key
794 _storage
.__jstorage_meta
.TTL
[key
] = curtime
+ ttl
;
796 delete _storage
.__jstorage_meta
.TTL
[key
];
810 * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
812 * @param {String} key Key to check
813 * @return {Number} Remaining TTL in milliseconds
815 getTTL: function(key
) {
816 var curtime
= +new Date(),
819 if (key
in _storage
&& _storage
.__jstorage_meta
.TTL
&& _storage
.__jstorage_meta
.TTL
[key
]) {
820 ttl
= _storage
.__jstorage_meta
.TTL
[key
] - curtime
;
827 * Deletes everything in cache.
829 * @return {Boolean} Always true
839 _fireObservers(null, 'flushed');
844 * Returns a read-only copy of _storage
846 * @return {Object} Read-only copy of _storage
848 storageObj: function() {
850 F
.prototype = _storage
;
855 * Returns an index of all used keys as an array
856 * ['key1', 'key2',..'keyN']
858 * @return {Array} Used keys
863 for (i
in _storage
) {
864 if (_storage
.hasOwnProperty(i
) && i
!= '__jstorage_meta') {
872 * How much space in bytes does the storage take?
874 * @return {Number} Storage size in chars (not the same as in bytes,
875 * since some chars may take several bytes)
877 storageSize: function() {
878 return _storage_size
;
882 * Which backend is currently in use?
884 * @return {String} Backend name
886 currentBackend: function() {
891 * Test if storage is available
893 * @return {Boolean} True if storage can be used
895 storageAvailable: function() {
900 * Register change listeners
902 * @param {String} key Key name
903 * @param {Function} callback Function to run when the key changes
905 listenKeyChange: function(key
, callback
) {
907 if (!_observers
[key
]) {
908 _observers
[key
] = [];
910 _observers
[key
].push(callback
);
914 * Remove change listeners
916 * @param {String} key Key name to unregister listeners against
917 * @param {Function} [callback] If set, unregister the callback, if not - unregister all
919 stopListening: function(key
, callback
) {
922 if (!_observers
[key
]) {
927 delete _observers
[key
];
931 for (var i
= _observers
[key
].length
- 1; i
>= 0; i
--) {
932 if (_observers
[key
][i
] == callback
) {
933 _observers
[key
].splice(i
, 1);
939 * Subscribe to a Publish/Subscribe event stream
941 * @param {String} channel Channel name
942 * @param {Function} callback Function to run when the something is published to the channel
944 subscribe: function(channel
, callback
) {
945 channel
= (channel
|| '').toString();
947 throw new TypeError('Channel not defined');
949 if (!_pubsub_observers
[channel
]) {
950 _pubsub_observers
[channel
] = [];
952 _pubsub_observers
[channel
].push(callback
);
956 * Publish data to an event stream
958 * @param {String} channel Channel name
959 * @param {Mixed} payload Payload to deliver
961 publish: function(channel
, payload
) {
962 channel
= (channel
|| '').toString();
964 throw new TypeError('Channel not defined');
967 _publish(channel
, payload
);
971 * Reloads the data from browser storage
978 * Removes reference from global objects and saves it as jStorage
980 * @param {Boolean} option if needed to save object as simple 'jStorage' in windows context
982 noConflict: function(saveInGlobal
) {
983 delete window
.$.jStorage
;
986 window
.jStorage
= this;
993 // Initialize jStorage