From ea437de10af995b2b833c81d1a2e92b72e2622d7 Mon Sep 17 00:00:00 2001 From: Stephen McKamey Date: Tue, 17 Jan 2012 11:32:17 -0800 Subject: [PATCH 01/13] jshint.com recommendations --- lscache.js | 40 +++++++++++++++++++++------------------- 1 file changed, 21 insertions(+), 19 deletions(-) diff --git a/lscache.js b/lscache.js index b3f0354..094a832 100644 --- a/lscache.js +++ b/lscache.js @@ -19,23 +19,25 @@ * Creates a namespace for the lscache functions. */ var lscache = function() { - // Prefixes the key name on the expiration items in localStorage + 'use strict'; + + // Suffixes the key name on the expiration items in localStorage var CACHESUFFIX = '-cacheexpiration'; // Determines if localStorage is supported in the browser; // result is cached for better performance instead of being run each time. // Feature detection is based on how Modernizr does it; // it's not straightforward due to FF4 issues. - var supportsStorage = function () { + var supportsStorage = (function(){ try { return !!localStorage.getItem; } catch (e) { return false; } - }(); + })(); // Determines if native JSON (de-)serialization is supported in the browser. - var supportsJSON = (window.JSON != null); + var supportsJSON = !!window.JSON; /** * Returns the full string for the localStorage expiration item. @@ -63,13 +65,13 @@ var lscache = function() { * @param {number} time */ set: function(key, value, time) { - if (!supportsStorage) return; + if (!supportsStorage) { return; } // If we don't get a string value, try to stringify // In future, localStorage may properly support storing non-strings // and this can be removed. - if (typeof value != 'string') { - if (!supportsJSON) return; + if (typeof value !== 'string') { + if (!supportsJSON) { return; } try { value = JSON.stringify(value); } catch (e) { @@ -82,11 +84,11 @@ var lscache = function() { try { localStorage.setItem(key, value); } catch (e) { - if (e.name === 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED') { + if (e.name === 'QUOTA_EXCEEDED_ERR' || e.name === 'NS_ERROR_DOM_QUOTA_REACHED') { // If we exceeded the quota, then we will sort // by the expire time, and then remove the N oldest var storedKey, storedKeys = []; - for (var i = 0; i < localStorage.length; i++) { + for (var i = 0, len = localStorage.length; i < len; i++) { storedKey = localStorage.key(i); if (storedKey.indexOf(CACHESUFFIX) > -1) { var mainKey = storedKey.split(CACHESUFFIX)[0]; @@ -95,7 +97,7 @@ var lscache = function() { } storedKeys.sort(function(a, b) { return (a.expiration-b.expiration); }); - for (var i = 0, len = Math.min(30, storedKeys.length); i < len; i++) { + for (i = 0, len = Math.min(30, storedKeys.length); i < len; i++) { localStorage.removeItem(storedKeys[i].key); localStorage.removeItem(expirationKey(storedKeys[i].key)); } @@ -122,7 +124,7 @@ var lscache = function() { * @return {string|Object} */ get: function(key) { - if (!supportsStorage) return null; + if (!supportsStorage) { return null; } /** * Tries to de-serialize stored value if its an object, and returns the @@ -130,18 +132,18 @@ var lscache = function() { * @param {String} key */ function parsedStorage(key) { - if (supportsJSON) { - try { + if (supportsJSON) { + try { // We can't tell if its JSON or a string, so we try to parse var value = JSON.parse(localStorage.getItem(key)); return value; - } catch(e) { + } catch(e) { // If we can't parse, it's probably because it isn't an object return localStorage.getItem(key); - } - } else { - return localStorage.getItem(key); - } + } + } else { + return localStorage.getItem(key); + } } // Return the de-serialized item if not expired @@ -167,7 +169,7 @@ var lscache = function() { * @param {string} key */ remove: function(key) { - if (!supportsStorage) return null; + if (!supportsStorage) { return null; } localStorage.removeItem(key); localStorage.removeItem(expirationKey(key)); } From 607666888627e489b27ef7bf1183eb51acc63696 Mon Sep 17 00:00:00 2001 From: Stephen McKamey Date: Tue, 17 Jan 2012 11:55:13 -0800 Subject: [PATCH 02/13] parameterize for tuning --- lscache.js | 20 +++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/lscache.js b/lscache.js index 094a832..7a98a00 100644 --- a/lscache.js +++ b/lscache.js @@ -21,9 +21,15 @@ var lscache = function() { 'use strict'; - // Suffixes the key name on the expiration items in localStorage + // Suffix for the key name on the expiration items in localStorage var CACHESUFFIX = '-cacheexpiration'; + // expiration date base (store as Base-36 for space savings) + var EXPIRY_BASE = 10; + + // time resolution in seconds + var EXPIRY_UNITS = 60 * 1000; + // Determines if localStorage is supported in the browser; // result is cached for better performance instead of being run each time. // Feature detection is based on how Modernizr does it; @@ -53,7 +59,7 @@ var lscache = function() { * @return {number} */ function currentTime() { - return Math.floor((new Date().getTime())/60000); + return Math.floor((new Date().getTime())/EXPIRY_UNITS); } return { @@ -90,9 +96,9 @@ var lscache = function() { var storedKey, storedKeys = []; for (var i = 0, len = localStorage.length; i < len; i++) { storedKey = localStorage.key(i); - if (storedKey.indexOf(CACHESUFFIX) > -1) { + if (storedKey.indexOf(CACHESUFFIX) >= 0) { var mainKey = storedKey.split(CACHESUFFIX)[0]; - storedKeys.push({key: mainKey, expiration: parseInt(localStorage[storedKey], 10)}); + storedKeys.push({key: mainKey, expiration: parseInt(localStorage[storedKey], EXPIRY_BASE)}); } } storedKeys.sort(function(a, b) { return (a.expiration-b.expiration); }); @@ -111,9 +117,9 @@ var lscache = function() { // If a time is specified, store expiration info in localStorage if (time) { - localStorage.setItem(expirationKey(key), currentTime() + time); + localStorage.setItem(expirationKey(key), (currentTime() + time).toString(EXPIRY_BASE)); } else { - // In case they set a time earlier, remove that info from localStorage. + // In case they previously set a time, remove that info from localStorage. localStorage.removeItem(expirationKey(key)); } }, @@ -148,7 +154,7 @@ var lscache = function() { // Return the de-serialized item if not expired if (localStorage.getItem(expirationKey(key))) { - var expirationTime = parseInt(localStorage.getItem(expirationKey(key)), 10); + var expirationTime = parseInt(localStorage.getItem(expirationKey(key)), EXPIRY_BASE); // Check if we should actually kick item out of storage if (currentTime() >= expirationTime) { localStorage.removeItem(key); From b952ad8fccb8222d2a694eb1d85ab029e20d1cce Mon Sep 17 00:00:00 2001 From: Stephen McKamey Date: Tue, 17 Jan 2012 12:03:08 -0800 Subject: [PATCH 03/13] reflow get for less repetition --- lscache.js | 49 +++++++++++++++++++++---------------------------- 1 file changed, 21 insertions(+), 28 deletions(-) diff --git a/lscache.js b/lscache.js index 7a98a00..e07e14c 100644 --- a/lscache.js +++ b/lscache.js @@ -132,41 +132,34 @@ var lscache = function() { get: function(key) { if (!supportsStorage) { return null; } - /** - * Tries to de-serialize stored value if its an object, and returns the - * normal value otherwise. - * @param {String} key - */ - function parsedStorage(key) { - if (supportsJSON) { - try { - // We can't tell if its JSON or a string, so we try to parse - var value = JSON.parse(localStorage.getItem(key)); - return value; - } catch(e) { - // If we can't parse, it's probably because it isn't an object - return localStorage.getItem(key); - } - } else { - return localStorage.getItem(key); - } - } - // Return the de-serialized item if not expired - if (localStorage.getItem(expirationKey(key))) { - var expirationTime = parseInt(localStorage.getItem(expirationKey(key)), EXPIRY_BASE); + var expr_key = expirationKey(key), + expr = localStorage.getItem(expr_key); + + if (expr) { + var expirationTime = parseInt(expr, EXPIRY_BASE); + // Check if we should actually kick item out of storage if (currentTime() >= expirationTime) { localStorage.removeItem(key); - localStorage.removeItem(expirationKey(key)); + localStorage.removeItem(expr_key); return null; - } else { - return parsedStorage(key); } - } else if (localStorage.getItem(key)) { - return parsedStorage(key); } - return null; + + // Tries to de-serialize stored value if its an object, and returns the normal value otherwise. + var value = localStorage.getItem(key); + if (!value || !supportsJSON) { + return value; + } + + try { + // We can't tell if its JSON or a string, so we try to parse + return JSON.parse(value); + } catch(e) { + // If we can't parse, it's probably because it isn't an object + return value; + } }, /** From c080d759a7b34cdee837bd614cf15fd80c9d6abf Mon Sep 17 00:00:00 2001 From: Stephen McKamey Date: Tue, 17 Jan 2012 12:08:37 -0800 Subject: [PATCH 04/13] removing items until item to be inserted fits --- lscache.js | 27 ++++++++++++++++++++------- 1 file changed, 20 insertions(+), 7 deletions(-) diff --git a/lscache.js b/lscache.js index e07e14c..730888c 100644 --- a/lscache.js +++ b/lscache.js @@ -27,7 +27,7 @@ var lscache = function() { // expiration date base (store as Base-36 for space savings) var EXPIRY_BASE = 10; - // time resolution in seconds + // time resolution in minutes var EXPIRY_UNITS = 60 * 1000; // Determines if localStorage is supported in the browser; @@ -78,6 +78,7 @@ var lscache = function() { // and this can be removed. if (typeof value !== 'string') { if (!supportsJSON) { return; } + try { value = JSON.stringify(value); } catch (e) { @@ -98,17 +99,29 @@ var lscache = function() { storedKey = localStorage.key(i); if (storedKey.indexOf(CACHESUFFIX) >= 0) { var mainKey = storedKey.split(CACHESUFFIX)[0]; - storedKeys.push({key: mainKey, expiration: parseInt(localStorage[storedKey], EXPIRY_BASE)}); + storedKeys.push({ + key: mainKey, + size: (localStorage[mainKey]||'').length, + expiration: parseInt(localStorage[storedKey], EXPIRY_BASE) + }); } } storedKeys.sort(function(a, b) { return (a.expiration-b.expiration); }); - for (i = 0, len = Math.min(30, storedKeys.length); i < len; i++) { - localStorage.removeItem(storedKeys[i].key); - localStorage.removeItem(expirationKey(storedKeys[i].key)); + var targetSize = (value||'').length; + while (storedKeys.length && targetSize > 0) { + storedKey = storedKeys.pop(); + localStorage.removeItem(storedKey.key); + localStorage.removeItem(expirationKey(storedKey.key)); + targetSize -= storedKey.size; + } + + try { + localStorage.setItem(key, value); + } catch(e) { + // value may be larger than total quota + return; } - // TODO: This could still error if the items we removed were small and this is large - localStorage.setItem(key, value); } else { // If it was some other error, just give up. return; From bb35ef0a51b7e756903b33768050e4eac70ac91c Mon Sep 17 00:00:00 2001 From: Stephen McKamey Date: Tue, 17 Jan 2012 14:57:23 -0800 Subject: [PATCH 05/13] minor tweaks --- lscache.js | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/lscache.js b/lscache.js index 730888c..d247265 100644 --- a/lscache.js +++ b/lscache.js @@ -18,7 +18,7 @@ /** * Creates a namespace for the lscache functions. */ -var lscache = function() { +var lscache = (function() { 'use strict'; // Suffix for the key name on the expiration items in localStorage @@ -34,7 +34,7 @@ var lscache = function() { // result is cached for better performance instead of being run each time. // Feature detection is based on how Modernizr does it; // it's not straightforward due to FF4 issues. - var supportsStorage = (function(){ + var supportsStorage = (function() { try { return !!localStorage.getItem; } catch (e) { @@ -74,8 +74,6 @@ var lscache = function() { if (!supportsStorage) { return; } // If we don't get a string value, try to stringify - // In future, localStorage may properly support storing non-strings - // and this can be removed. if (typeof value !== 'string') { if (!supportsJSON) { return; } @@ -97,16 +95,17 @@ var lscache = function() { var storedKey, storedKeys = []; for (var i = 0, len = localStorage.length; i < len; i++) { storedKey = localStorage.key(i); - if (storedKey.indexOf(CACHESUFFIX) >= 0) { - var mainKey = storedKey.split(CACHESUFFIX)[0]; + var suffix = storedKey.indexOf(CACHESUFFIX); + if (suffix >= 0) { + var mainKey = storedKey.substr(0, suffix); storedKeys.push({ key: mainKey, size: (localStorage[mainKey]||'').length, - expiration: parseInt(localStorage[storedKey], EXPIRY_BASE) + expr: parseInt(localStorage[storedKey], EXPIRY_BASE) }); } } - storedKeys.sort(function(a, b) { return (a.expiration-b.expiration); }); + storedKeys.sort(function(a, b) { return (a.expr-b.expr); }); var targetSize = (value||'').length; while (storedKeys.length && targetSize > 0) { @@ -181,9 +180,9 @@ var lscache = function() { * @param {string} key */ remove: function(key) { - if (!supportsStorage) { return null; } + if (!supportsStorage) { return; } localStorage.removeItem(key); localStorage.removeItem(expirationKey(key)); } }; -}(); +})(); From ceb3b19b208154722516ef84d1d566ad6a2740be Mon Sep 17 00:00:00 2001 From: Stephen McKamey Date: Tue, 17 Jan 2012 15:16:11 -0800 Subject: [PATCH 06/13] clear localStorage before tests --- test.html | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/test.html b/test.html index c983810..dea5e9c 100644 --- a/test.html +++ b/test.html @@ -5,6 +5,11 @@