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/>
39 /* jStorage version */
40 JSTORAGE_VERSION
= "0.4.8",
42 /* detect a dollar object or create one if not found */
43 $ = window
.jQuery
|| window
.$ || (window
.$ = {}),
45 /* check for a JSON handling support */
48 window
.JSON
&& (window
.JSON
.parse
|| window
.JSON
.decode
) ||
49 String
.prototype.evalJSON
&& function(str
){return String(str
).evalJSON();} ||
54 window
.JSON
&& (window
.JSON
.stringify
|| window
.JSON
.encode
) ||
58 // Break if no JSON support was found
59 if(!("parse" in JSON
) || !("stringify" in JSON
)){
60 throw new Error("No JSON support found, include //cdnjs.cloudflare.com/ajax/libs/json2/20110223/json2.js to page");
64 /* This is the object, that holds the cached values */
65 _storage
= {__jstorage_meta
:{CRC32
:{}}},
67 /* Actual browser storage (localStorage or globalStorage["domain"]) */
68 _storage_service
= {jStorage
:"{}"},
70 /* DOM element for older IE versions, holds userData behavior */
73 /* How much space does the storage take */
76 /* which backend is currently used */
79 /* onchange observers */
82 /* timeout to wait after onchange event */
83 _observer_timeout
= false,
85 /* last update time */
88 /* pubsub observers */
89 _pubsub_observers
= {},
91 /* skip published items older than current timestamp */
92 _pubsub_last
= +new Date(),
94 /* Next check for TTL */
98 * XML encoding and decoding as XML nodes can't be JSON'ized
99 * XML nodes are encoded and decoded if the node is the value to be saved
100 * but not if it's as a property of another object
102 * $.jStorage.set("key", xmlNode); // IS OK
103 * $.jStorage.set("key", {xml: xmlNode}); // NOT OK
108 * Validates a XML node to be XML
109 * based on jQuery.isXML function
111 isXML: function(elm
){
112 var documentElement
= (elm
? elm
.ownerDocument
|| elm
: 0).documentElement
;
113 return documentElement
? documentElement
.nodeName
!== "HTML" : false;
117 * Encodes a XML node to string
118 * based on http://www.mercurytide.co.uk/news/article/issues-when-working-ajax/
120 encode: function(xmlNode
) {
121 if(!this.isXML(xmlNode
)){
124 try{ // Mozilla, Webkit, Opera
125 return new XMLSerializer().serializeToString(xmlNode
);
135 * Decodes a XML node from string
136 * loosely based on http://outwestmedia.com/jquery-plugins/xmldom/
138 decode: function(xmlString
){
139 var dom_parser
= ("DOMParser" in window
&& (new DOMParser()).parseFromString
) ||
140 (window
.ActiveXObject
&& function(_xmlString
) {
141 var xml_doc
= new ActiveXObject("Microsoft.XMLDOM");
142 xml_doc
.async
= "false";
143 xml_doc
.loadXML(_xmlString
);
150 resultXML
= dom_parser
.call("DOMParser" in window
&& (new DOMParser()) || window
, xmlString
, "text/xml");
151 return this.isXML(resultXML
)?resultXML
:false;
156 ////////////////////////// PRIVATE METHODS ////////////////////////
159 * Initialization function. Detects if the browser supports DOM Storage
160 * or userData behavior and behaves accordingly.
163 /* Check if browser supports localStorage */
164 var localStorageReallyWorks
= false;
165 if("localStorage" in window
){
167 window
.localStorage
.setItem("_tmptest", "tmpval");
168 localStorageReallyWorks
= true;
169 window
.localStorage
.removeItem("_tmptest");
170 } catch(BogusQuotaExceededErrorOnIos5
) {
171 // Thanks be to iOS5 Private Browsing mode which throws
172 // QUOTA_EXCEEDED_ERRROR DOM Exception 22.
176 if(localStorageReallyWorks
){
178 if(window
.localStorage
) {
179 _storage_service
= window
.localStorage
;
180 _backend
= "localStorage";
181 _observer_update
= _storage_service
.jStorage_update
;
183 } catch(E3
) {/* Firefox fails when touching localStorage and cookies are disabled */}
185 /* Check if browser supports globalStorage */
186 else if("globalStorage" in window
){
188 if(window
.globalStorage
) {
189 if(window
.location
.hostname
== "localhost"){
190 _storage_service
= window
.globalStorage
["localhost.localdomain"];
193 _storage_service
= window
.globalStorage
[window
.location
.hostname
];
195 _backend
= "globalStorage";
196 _observer_update
= _storage_service
.jStorage_update
;
198 } catch(E4
) {/* Firefox fails when touching localStorage and cookies are disabled */}
200 /* Check if browser supports userData behavior */
202 _storage_elm
= document
.createElement("link");
203 if(_storage_elm
.addBehavior
){
205 /* Use a DOM element to act as userData storage */
206 _storage_elm
.style
.behavior
= "url(#default#userData)";
208 /* userData element needs to be inserted into the DOM! */
209 document
.getElementsByTagName("head")[0].appendChild(_storage_elm
);
212 _storage_elm
.load("jStorage");
214 // try to reset cache
215 _storage_elm
.setAttribute("jStorage", "{}");
216 _storage_elm
.save("jStorage");
217 _storage_elm
.load("jStorage");
222 data
= _storage_elm
.getAttribute("jStorage");
226 _observer_update
= _storage_elm
.getAttribute("jStorage_update");
229 _storage_service
.jStorage
= data
;
230 _backend
= "userDataBehavior";
237 // Load data from storage
243 // start listening for changes
246 // initialize publish-subscribe service
249 // handle cached navigation
250 if("addEventListener" in window
){
251 window
.addEventListener("pageshow", function(event
){
260 * Reload data from storage when needed
262 function _reloadData(){
265 if(_backend
== "userDataBehavior"){
266 _storage_elm
.load("jStorage");
269 data
= _storage_elm
.getAttribute("jStorage");
273 _observer_update
= _storage_elm
.getAttribute("jStorage_update");
276 _storage_service
.jStorage
= data
;
288 * Sets up a storage change observer
290 function _setupObserver(){
291 if(_backend
== "localStorage" || _backend
== "globalStorage"){
292 if("addEventListener" in window
){
293 window
.addEventListener("storage", _storageObserver
, false);
295 document
.attachEvent("onstorage", _storageObserver
);
297 }else if(_backend
== "userDataBehavior"){
298 setInterval(_storageObserver
, 1000);
303 * Fired on any kind of data change, needs to check if anything has
304 * really been changed
306 function _storageObserver(){
308 // cumulate change notifications with timeout
309 clearTimeout(_observer_timeout
);
310 _observer_timeout
= setTimeout(function(){
312 if(_backend
== "localStorage" || _backend
== "globalStorage"){
313 updateTime
= _storage_service
.jStorage_update
;
314 }else if(_backend
== "userDataBehavior"){
315 _storage_elm
.load("jStorage");
317 updateTime
= _storage_elm
.getAttribute("jStorage_update");
321 if(updateTime
&& updateTime
!= _observer_update
){
322 _observer_update
= updateTime
;
330 * Reloads the data and checks if any keys are changed
332 function _checkUpdatedKeys(){
333 var oldCrc32List
= JSON
.parse(JSON
.stringify(_storage
.__jstorage_meta
.CRC32
)),
337 newCrc32List
= JSON
.parse(JSON
.stringify(_storage
.__jstorage_meta
.CRC32
));
343 for(key
in oldCrc32List
){
344 if(oldCrc32List
.hasOwnProperty(key
)){
345 if(!newCrc32List
[key
]){
349 if(oldCrc32List
[key
] != newCrc32List
[key
] && String(oldCrc32List
[key
]).substr(0,2) == "2."){
355 for(key
in newCrc32List
){
356 if(newCrc32List
.hasOwnProperty(key
)){
357 if(!oldCrc32List
[key
]){
363 _fireObservers(updated
, "updated");
364 _fireObservers(removed
, "deleted");
368 * Fires observers for updated keys
370 * @param {Array|String} keys Array of key names or a key
371 * @param {String} action What happened with the value (updated, deleted, flushed)
373 function _fireObservers(keys
, action
){
374 keys
= [].concat(keys
|| []);
375 if(action
== "flushed"){
377 for(var key
in _observers
){
378 if(_observers
.hasOwnProperty(key
)){
384 for(var i
=0, len
= keys
.length
; i
<len
; i
++){
385 if(_observers
[keys
[i
]]){
386 for(var j
=0, jlen
= _observers
[keys
[i
]].length
; j
<jlen
; j
++){
387 _observers
[keys
[i
]][j
](keys
[i
], action
);
391 for(var j
=0, jlen
= _observers
["*"].length
; j
<jlen
; j
++){
392 _observers
["*"][j
](keys
[i
], action
);
399 * Publishes key change to listeners
401 function _publishChange(){
402 var updateTime
= (+new Date()).toString();
404 if(_backend
== "localStorage" || _backend
== "globalStorage"){
406 _storage_service
.jStorage_update
= updateTime
;
408 // safari private mode has been enabled after the jStorage initialization
411 }else if(_backend
== "userDataBehavior"){
412 _storage_elm
.setAttribute("jStorage_update", updateTime
);
413 _storage_elm
.save("jStorage");
420 * Loads the data from the storage based on the supported mechanism
422 function _load_storage(){
423 /* if jStorage string is retrieved, then decode it */
424 if(_storage_service
.jStorage
){
426 _storage
= JSON
.parse(String(_storage_service
.jStorage
));
427 }catch(E6
){_storage_service
.jStorage
= "{}";}
429 _storage_service
.jStorage
= "{}";
431 _storage_size
= _storage_service
.jStorage
?String(_storage_service
.jStorage
).length
:0;
433 if(!_storage
.__jstorage_meta
){
434 _storage
.__jstorage_meta
= {};
436 if(!_storage
.__jstorage_meta
.CRC32
){
437 _storage
.__jstorage_meta
.CRC32
= {};
442 * This functions provides the "save" mechanism to store the jStorage object
445 _dropOldEvents(); // remove expired events
447 _storage_service
.jStorage
= JSON
.stringify(_storage
);
448 // If userData is used as the storage engine, additional
450 _storage_elm
.setAttribute("jStorage",_storage_service
.jStorage
);
451 _storage_elm
.save("jStorage");
453 _storage_size
= _storage_service
.jStorage
?String(_storage_service
.jStorage
).length
:0;
454 }catch(E7
){/* probably cache is full, nothing is saved this way*/}
458 * Function checks if a key is set and is string or numberic
460 * @param {String} key Key name
462 function _checkKey(key
){
463 if(typeof key
!= "string" && typeof key
!= "number"){
464 throw new TypeError("Key name must be string or numeric");
466 if(key
== "__jstorage_meta"){
467 throw new TypeError("Reserved key name");
473 * Removes expired keys
475 function _handleTTL(){
476 var curtime
, i
, TTL
, CRC32
, nextExpire
= Infinity
, changed
= false, deleted
= [];
478 clearTimeout(_ttl_timeout
);
480 if(!_storage
.__jstorage_meta
|| typeof _storage
.__jstorage_meta
.TTL
!= "object"){
481 // nothing to do here
485 curtime
= +new Date();
486 TTL
= _storage
.__jstorage_meta
.TTL
;
488 CRC32
= _storage
.__jstorage_meta
.CRC32
;
490 if(TTL
.hasOwnProperty(i
)){
491 if(TTL
[i
] <= curtime
){
497 }else if(TTL
[i
] < nextExpire
){
504 if(nextExpire
!= Infinity
){
505 _ttl_timeout
= setTimeout(_handleTTL
, nextExpire
- curtime
);
512 _fireObservers(deleted
, "deleted");
517 * Checks if there's any events on hold to be fired to listeners
519 function _handlePubSub(){
521 if(!_storage
.__jstorage_meta
.PubSub
){
525 _pubsubCurrent
= _pubsub_last
;
527 for(i
=len
=_storage
.__jstorage_meta
.PubSub
.length
-1; i
>=0; i
--){
528 pubelm
= _storage
.__jstorage_meta
.PubSub
[i
];
529 if(pubelm
[0] > _pubsub_last
){
530 _pubsubCurrent
= pubelm
[0];
531 _fireSubscribers(pubelm
[1], pubelm
[2]);
535 _pubsub_last
= _pubsubCurrent
;
539 * Fires all subscriber listeners for a pubsub channel
541 * @param {String} channel Channel name
542 * @param {Mixed} payload Payload data to deliver
544 function _fireSubscribers(channel
, payload
){
545 if(_pubsub_observers
[channel
]){
546 for(var i
=0, len
= _pubsub_observers
[channel
].length
; i
<len
; i
++){
547 // send immutable data that can't be modified by listeners
549 _pubsub_observers
[channel
][i
](channel
, JSON
.parse(JSON
.stringify(payload
)));
556 * Remove old events from the publish stream (at least 2sec old)
558 function _dropOldEvents(){
559 if(!_storage
.__jstorage_meta
.PubSub
){
563 var retire
= +new Date() - 2000;
565 for(var i
=0, len
= _storage
.__jstorage_meta
.PubSub
.length
; i
<len
; i
++){
566 if(_storage
.__jstorage_meta
.PubSub
[i
][0] <= retire
){
567 // deleteCount is needed for IE6
568 _storage
.__jstorage_meta
.PubSub
.splice(i
, _storage
.__jstorage_meta
.PubSub
.length
- i
);
573 if(!_storage
.__jstorage_meta
.PubSub
.length
){
574 delete _storage
.__jstorage_meta
.PubSub
;
580 * Publish payload to a channel
582 * @param {String} channel Channel name
583 * @param {Mixed} payload Payload to send to the subscribers
585 function _publish(channel
, payload
){
586 if(!_storage
.__jstorage_meta
){
587 _storage
.__jstorage_meta
= {};
589 if(!_storage
.__jstorage_meta
.PubSub
){
590 _storage
.__jstorage_meta
.PubSub
= [];
593 _storage
.__jstorage_meta
.PubSub
.unshift([+new Date
, channel
, payload
]);
601 * JS Implementation of MurmurHash2
603 * SOURCE: https://github.com/garycourt/murmurhash-js (MIT licensed)
605 * @author <a href="mailto:gary.court@gmail.com">Gary Court</a>
606 * @see http://github.com/garycourt/murmurhash-js
607 * @author <a href="mailto:aappleby@gmail.com">Austin Appleby</a>
608 * @see http://sites.google.com/site/murmurhash/
610 * @param {string} str ASCII only
611 * @param {number} seed Positive integer only
612 * @return {number} 32-bit positive integer hash
615 function murmurhash2_32_gc(str
, seed
) {
624 ((str
.charCodeAt(i
) & 0xff)) |
625 ((str
.charCodeAt(++i
) & 0xff) << 8) |
626 ((str
.charCodeAt(++i
) & 0xff) << 16) |
627 ((str
.charCodeAt(++i
) & 0xff) << 24);
629 k
= (((k
& 0xffff) * 0x5bd1e995) + ((((k
>>> 16) * 0x5bd1e995) & 0xffff) << 16));
631 k
= (((k
& 0xffff) * 0x5bd1e995) + ((((k
>>> 16) * 0x5bd1e995) & 0xffff) << 16));
633 h
= (((h
& 0xffff) * 0x5bd1e995) + ((((h
>>> 16) * 0x5bd1e995) & 0xffff) << 16)) ^ k
;
640 case 3: h
^= (str
.charCodeAt(i
+ 2) & 0xff) << 16;
641 case 2: h
^= (str
.charCodeAt(i
+ 1) & 0xff) << 8;
642 case 1: h
^= (str
.charCodeAt(i
) & 0xff);
643 h
= (((h
& 0xffff) * 0x5bd1e995) + ((((h
>>> 16) * 0x5bd1e995) & 0xffff) << 16));
647 h
= (((h
& 0xffff) * 0x5bd1e995) + ((((h
>>> 16) * 0x5bd1e995) & 0xffff) << 16));
653 ////////////////////////// PUBLIC INTERFACE /////////////////////////
657 version
: JSTORAGE_VERSION
,
660 * Sets a key's value.
662 * @param {String} key Key to set. If this value is not set or not
663 * a string an exception is raised.
664 * @param {Mixed} value Value to set. This can be any value that is JSON
665 * compatible (Numbers, Strings, Objects etc.).
666 * @param {Object} [options] - possible options to use
667 * @param {Number} [options.TTL] - optional TTL value
668 * @return {Mixed} the used value
670 set: function(key
, value
, options
){
673 options
= options
|| {};
675 // undefined values are deleted automatically
676 if(typeof value
== "undefined"){
681 if(_XMLService
.isXML(value
)){
682 value
= {_is_xml
:true,xml
:_XMLService
.encode(value
)};
683 }else if(typeof value
== "function"){
684 return undefined; // functions can't be saved!
685 }else if(value
&& typeof value
== "object"){
686 // clone the object before saving to _storage tree
687 value
= JSON
.parse(JSON
.stringify(value
));
690 _storage
[key
] = value
;
692 _storage
.__jstorage_meta
.CRC32
[key
] = "2." + murmurhash2_32_gc(JSON
.stringify(value
), 0x9747b28c);
694 this.setTTL(key
, options
.TTL
|| 0); // also handles saving and _publishChange
696 _fireObservers(key
, "updated");
701 * Looks up a key in cache
703 * @param {String} key - Key to look up.
704 * @param {mixed} def - Default value to return, if key didn't exist.
705 * @return {Mixed} the key value, default value or null
707 get: function(key
, def
){
710 if(_storage
[key
] && typeof _storage
[key
] == "object" && _storage
[key
]._is_xml
) {
711 return _XMLService
.decode(_storage
[key
].xml
);
713 return _storage
[key
];
716 return typeof(def
) == "undefined" ? null : def
;
720 * Deletes a key from cache.
722 * @param {String} key - Key to delete.
723 * @return {Boolean} true if key existed or false if it didn't
725 deleteKey: function(key
){
728 delete _storage
[key
];
729 // remove from TTL list
730 if(typeof _storage
.__jstorage_meta
.TTL
== "object" &&
731 key
in _storage
.__jstorage_meta
.TTL
){
732 delete _storage
.__jstorage_meta
.TTL
[key
];
735 delete _storage
.__jstorage_meta
.CRC32
[key
];
739 _fireObservers(key
, "deleted");
746 * Sets a TTL for a key, or remove it if ttl value is 0 or below
748 * @param {String} key - key to set the TTL for
749 * @param {Number} ttl - TTL timeout in milliseconds
750 * @return {Boolean} true if key existed or false if it didn't
752 setTTL: function(key
, ttl
){
753 var curtime
= +new Date();
755 ttl
= Number(ttl
) || 0;
758 if(!_storage
.__jstorage_meta
.TTL
){
759 _storage
.__jstorage_meta
.TTL
= {};
762 // Set TTL value for the key
764 _storage
.__jstorage_meta
.TTL
[key
] = curtime
+ ttl
;
766 delete _storage
.__jstorage_meta
.TTL
[key
];
780 * Gets remaining TTL (in milliseconds) for a key or 0 when no TTL has been set
782 * @param {String} key Key to check
783 * @return {Number} Remaining TTL in milliseconds
785 getTTL: function(key
){
786 var curtime
= +new Date(), ttl
;
788 if(key
in _storage
&& _storage
.__jstorage_meta
.TTL
&& _storage
.__jstorage_meta
.TTL
[key
]){
789 ttl
= _storage
.__jstorage_meta
.TTL
[key
] - curtime
;
796 * Deletes everything in cache.
798 * @return {Boolean} Always true
801 _storage
= {__jstorage_meta
:{CRC32
:{}}};
804 _fireObservers(null, "flushed");
809 * Returns a read-only copy of _storage
811 * @return {Object} Read-only copy of _storage
813 storageObj: function(){
815 F
.prototype = _storage
;
820 * Returns an index of all used keys as an array
821 * ["key1", "key2",.."keyN"]
823 * @return {Array} Used keys
828 if(_storage
.hasOwnProperty(i
) && i
!= "__jstorage_meta"){
836 * How much space in bytes does the storage take?
838 * @return {Number} Storage size in chars (not the same as in bytes,
839 * since some chars may take several bytes)
841 storageSize: function(){
842 return _storage_size
;
846 * Which backend is currently in use?
848 * @return {String} Backend name
850 currentBackend: function(){
855 * Test if storage is available
857 * @return {Boolean} True if storage can be used
859 storageAvailable: function(){
864 * Register change listeners
866 * @param {String} key Key name
867 * @param {Function} callback Function to run when the key changes
869 listenKeyChange: function(key
, callback
){
871 if(!_observers
[key
]){
872 _observers
[key
] = [];
874 _observers
[key
].push(callback
);
878 * Remove change listeners
880 * @param {String} key Key name to unregister listeners against
881 * @param {Function} [callback] If set, unregister the callback, if not - unregister all
883 stopListening: function(key
, callback
){
886 if(!_observers
[key
]){
891 delete _observers
[key
];
895 for(var i
= _observers
[key
].length
- 1; i
>=0; i
--){
896 if(_observers
[key
][i
] == callback
){
897 _observers
[key
].splice(i
,1);
903 * Subscribe to a Publish/Subscribe event stream
905 * @param {String} channel Channel name
906 * @param {Function} callback Function to run when the something is published to the channel
908 subscribe: function(channel
, callback
){
909 channel
= (channel
|| "").toString();
911 throw new TypeError("Channel not defined");
913 if(!_pubsub_observers
[channel
]){
914 _pubsub_observers
[channel
] = [];
916 _pubsub_observers
[channel
].push(callback
);
920 * Publish data to an event stream
922 * @param {String} channel Channel name
923 * @param {Mixed} payload Payload to deliver
925 publish: function(channel
, payload
){
926 channel
= (channel
|| "").toString();
928 throw new TypeError("Channel not defined");
931 _publish(channel
, payload
);
935 * Reloads the data from browser storage
942 * Removes reference from global objects and saves it as jStorage
944 * @param {Boolean} option if needed to save object as simple "jStorage" in windows context
946 noConflict: function( saveInGlobal
) {
947 delete window
.$.jStorage
949 if ( saveInGlobal
) {
950 window
.jStorage
= this;
957 // Initialize jStorage