jquery.jStorage.js 31 KB

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