Skip to content
Browse files

fixes #113 adds new lscache, and retools inject to use the newly avai…

…lable lscache modules. Removes schemaVersion, which wasn't being used.
  • Loading branch information...
1 parent e7d141a commit ce45216f38f50fd43b4688da2d0e764c7a4e60eb @Jakobo committed May 29, 2012
Showing with 206 additions and 171 deletions.
  1. +18 −25 src/inject.coffee
  2. +188 −146 src/lscache.js
View
43 src/inject.coffee
@@ -52,13 +52,11 @@ onErrorOffset = 0 # offset for onerror calls
funcCount = 0 # functions initialized to date
userConfig = {} # user configuration options (see reset)
undef = undef # undefined
-schemaVersion = 1 # version of inject()'s localstorage schema
context = this # context is our local scope. Should be "window"
pauseRequired = false # can we run immediately? when using iframe transport, the answer is no
_db = {} # internal database of modules and transactions (see reset)
xDomainRpc = null # a cross domain RPC object (Porthole)
-fileStorageToken = "FILEDB" # a storagetoken identifier we use (lscache)
-fileStore = "Inject FileStorage" # file store to use
+fileStorageToken = "INJECT" # a storagetoken identifier we use (lscache)
namespace = "Inject" # the namespace for inject() that is publicly reachable
userModules = {} # any mappings for module => handling defined by the user
fileSuffix = /.*?\.(js|txt)(\?.*)?$/# Regex for identifying things that end in *.js or *.txt
@@ -84,6 +82,12 @@ commentRegex = /(\/\*([\s\S]*?)\*\/|([^:]|^)\/\/(.*)$)/mg
relativePathRegex = /^(.\/|..\/).*/
###
+lscache configuration
+sets up lscache to operate within the local scope
+###
+lscache.setBucket(fileStorageToken)
+
+###
CommonJS wrappers for a header and footer
these bookend the included code and insulate the scope so that it doesn't impact inject()
or anything else.
@@ -273,12 +277,11 @@ db = {
###
registry = _db.moduleRegistry
path = db.module.getPath(moduleId)
- token = "#{fileStorageToken}#{schemaVersion}#{path}"
if registry[moduleId]?.file then return registry[moduleId].file
if userConfig.fileExpires is 0 then return false
- file = lscache.get(token)
+ file = lscache.get(path)
if file and typeof(file) is "string" and file.length
db.module.setFile(moduleId, file)
return file
@@ -292,8 +295,7 @@ db = {
db.module.create(moduleId)
registry[moduleId].file = file
path = db.module.getPath(moduleId)
- token = "#{fileStorageToken}#{schemaVersion}#{path}"
- lscache.set(token, file, userConfig.fileExpires)
+ lscache.set(path, file, userConfig.fileExpires)
"clearAllFiles": () ->
###
## clearAllFiles() ##
@@ -614,26 +616,17 @@ reset = () ->
reset()
-clearFileRegistry = (version = schemaVersion) ->
+clearFileRegistry = () ->
###
- ## clearFileRegistry(version = schemaVersion) ##
- _internal_ Clears the internal file registry at `version`
- clearing all local storage keys that relate to the fileStorageToken and version
+ ## clearFileRegistry() ##
+ _internal_ Clears the internal file registry
+ clearing all local storage keys that relate to the fileStorageToken
###
if ! ('localStorage' in context) then return
-
- token = "#{fileStorageToken}#{version}"
- `
- for (var i = 0; i < localStorage.length; i++) {
- var key = localStorage.key(i);
- if (key.indexOf(token) !== -1) {
- localStorage.removeItem(key)
- }
- }
- `
- if version is schemaVersion then db.module.clearAllFiles()
+ db.module.clearAllFiles()
+ lscache.flush()
createIframe = () ->
###
@@ -1282,12 +1275,12 @@ require.setCrossDomain = (local, remote) ->
userConfig.xd.inject = local
userConfig.xd.xhr = remote
-require.clearCache = (version) ->
+require.clearCache = () ->
###
- ## require.clearCache(version) ##
+ ## require.clearCache() ##
Remove the localStorage class at version. If no version is specified, the entire cache is cleared.
###
- clearFileRegistry(version)
+ clearFileRegistry()
require.manifest = (manifest) ->
###
View
334 src/lscache.js
@@ -15,139 +15,97 @@
* limitations under the License.
*/
+/*jshint undef:true, browser:true */
+
/**
* Creates a namespace for the lscache functions.
*/
var lscache = function() {
- // Suffixes the key name on the expiration items in localStorage
- // shortened to help save space
- var CACHESUFFIX = '-EXP',
- TOUCHEDSUFFIX = '-LRU';
+
+ // Prefix for all lscache keys
+ var CACHE_PREFIX = 'lscache-';
+
+ // Suffix for the key name on the expiration items in localStorage
+ var CACHE_SUFFIX = '-cacheexpiration';
+
+ // expiration date radix (set to Base-36 for most space savings)
+ var EXPIRY_RADIX = 10;
+
+ // time resolution in minutes
+ var EXPIRY_UNITS = 60 * 1000;
+
+ // ECMAScript max Date (epoch + 1e8 days)
+ var MAX_DATE = Math.floor(8.64e15/EXPIRY_UNITS);
+
+ var cachedStorage;
+ var cachedJSON;
+ var cacheBucket = '';
// 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 () {
+ // It's not run at parse-time as it takes 200ms in Android.
+ function supportsStorage() {
+ var key = '__lscachetest__';
+ var value = key;
+
+ if (cachedStorage !== undefined) {
+ return cachedStorage;
+ }
+
try {
- return !!localStorage.getItem;
- } catch (e) {
- return false;
+ setItem(key, value);
+ removeItem(key);
+ cachedStorage = true;
+ } catch (exc) {
+ cachedStorage = false;
}
- }();
+ return cachedStorage;
+ }
// Determines if native JSON (de-)serialization is supported in the browser.
- var supportsJSON = (window.JSON != null);
+ function supportsJSON() {
+ /*jshint eqnull:true */
+ if (cachedJSON === undefined) {
+ cachedJSON = (window.JSON != null);
+ }
+ return cachedJSON;
+ }
/**
* Returns the full string for the localStorage expiration item.
* @param {String} key
* @return {string}
*/
function expirationKey(key) {
- return key + CACHESUFFIX;
- }
-
- /**
- * Returns the full string for the localStorage last access item
- * @param {String} key
- * @return {string}
- */
- function touchedKey(key) {
- return key + TOUCHEDSUFFIX;
+ return key + CACHE_SUFFIX;
}
/**
* Returns the number of minutes since the epoch.
* @return {number}
*/
function currentTime() {
- return Math.floor((new Date().getTime())/60000);
+ return Math.floor((new Date().getTime())/EXPIRY_UNITS);
}
-
- function attemptStorage(key, value, time) {
- var purgeSize = 1,
- sorted = false,
- firstTry = true,
- storedKeys = [],
- storedKey,
- removeItem;
-
- // start the retry loop until we can store
- retryLoop();
-
- function retryLoop() {
- try {
- // store into the touchedKey first. This way, if we overflow, we always
- // have the smallest units for reduction
- localStorage.setItem(touchedKey(key), currentTime());
-
- if (time > 0) {
- // if time is set, then add an expires key
- localStorage.setItem(expirationKey(key), currentTime() + time);
- localStorage.setItem(key, value);
- }
- else if (time < 0 || time === 0) {
- // if time is in the past or explictly 0, it's auto-expired
- // remove the key and return
- localStorage.removeItem(touchedKey(key));
- localStorage.removeItem(expirationKey(key));
- localStorage.removeItem(key);
- return;
- }
- else {
- // no time is set, it was a "forever" setting
- localStorage.setItem(key, value);
- }
- }
- catch(e) {
- if (e.name === 'QUOTA_EXCEEDED_ERR' || e.name == 'NS_ERROR_DOM_QUOTA_REACHED') {
- // if we fail and there's nothing in localstorage, then
- // there is simply too much trying to be stored (> 5mb) and we fail it quietly
- if (storedKeys.length === 0 && !firstTry) {
- localStorage.removeItem(touchedKey(key));
- localStorage.removeItem(expirationKey(key));
- localStorage.removeItem(key);
- return false;
- }
-
- // firstTry logic ensures we don't test for size conditions
- // until the second+ time through
- if (firstTry) {
- firstTry = false;
- }
-
- // If we exceeded the quota, then we will sort
- // by the expire time, and then remove the N oldest
- if (!sorted) {
- for (var i = 0, len = localStorage.length; i < len; i++) {
- storedKey = localStorage.key(i);
- if (storedKey.indexOf(TOUCHEDSUFFIX) > -1) {
- var mainKey = storedKey.split(TOUCHEDSUFFIX)[0];
- storedKeys.push({key: mainKey, touched: parseInt(localStorage.getItem(storedKey), 10)});
- }
- }
- storedKeys.sort(function(a, b) { return (a.touched-b.touched); });
- sorted = true;
- }
-
- // LRU
- removeItem = storedKeys.shift();
- if (removeItem) {
- localStorage.removeItem(touchedKey(removeItem.key));
- localStorage.removeItem(expirationKey(removeItem.key));
- localStorage.removeItem(removeItem.key);
- }
-
- // try again (currently recursive)
- retryLoop();
- }
- else {
- // this was some other error. Give up
- return;
- }
- }
- }
+
+ /**
+ * Wrapper functions for localStorage methods
+ */
+
+ function getItem(key) {
+ return localStorage.getItem(CACHE_PREFIX + cacheBucket + key);
+ }
+
+ function setItem(key, value) {
+ // Fix for iPad issue - sometimes throws QUOTA_EXCEEDED_ERR on setItem.
+ localStorage.removeItem(CACHE_PREFIX + cacheBucket + key);
+ localStorage.setItem(CACHE_PREFIX + cacheBucket + key, value);
+ }
+
+ function removeItem(key) {
+ localStorage.removeItem(CACHE_PREFIX + cacheBucket + key);
}
return {
@@ -159,13 +117,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) {
@@ -175,7 +133,63 @@ var lscache = function() {
}
}
- attemptStorage(key, value, time);
+ try {
+ setItem(key, value);
+ } catch (e) {
+ 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 storedKeys = [];
+ var storedKey;
+ for (var i = 0; i < localStorage.length; i++) {
+ storedKey = localStorage.key(i);
+
+ if (storedKey.indexOf(CACHE_PREFIX + cacheBucket) === 0 && storedKey.indexOf(CACHE_SUFFIX) < 0) {
+ var mainKey = storedKey.substr((CACHE_PREFIX + cacheBucket).length);
+ var exprKey = expirationKey(mainKey);
+ var expiration = getItem(exprKey);
+ if (expiration) {
+ expiration = parseInt(expiration, EXPIRY_RADIX);
+ } else {
+ // TODO: Store date added for non-expiring items for smarter removal
+ expiration = MAX_DATE;
+ }
+ storedKeys.push({
+ key: mainKey,
+ size: (getItem(mainKey)||'').length,
+ expiration: expiration
+ });
+ }
+ }
+ // Sorts the keys with oldest expiration time last
+ storedKeys.sort(function(a, b) { return (b.expiration-a.expiration); });
+
+ var targetSize = (value||'').length;
+ while (storedKeys.length && targetSize > 0) {
+ storedKey = storedKeys.pop();
+ removeItem(storedKey.key);
+ removeItem(expirationKey(storedKey.key));
+ targetSize -= storedKey.size;
+ }
+ try {
+ setItem(key, value);
+ } catch (e) {
+ // value may be larger than total quota
+ return;
+ }
+ } else {
+ // If it was some other error, just give up.
+ return;
+ }
+ }
+
+ // If a time is specified, store expiration info in localStorage
+ if (time) {
+ setItem(expirationKey(key), (currentTime() + time).toString(EXPIRY_RADIX));
+ } else {
+ // In case they previously set a time, remove that info from localStorage.
+ removeItem(expirationKey(key));
+ }
},
/**
@@ -184,46 +198,36 @@ var lscache = function() {
* @return {string|Object}
*/
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);
- }
- }
+ if (!supportsStorage()) return null;
// Return the de-serialized item if not expired
- if (localStorage.getItem(expirationKey(key))) {
- var expirationTime = parseInt(localStorage.getItem(expirationKey(key)), 10);
+ var exprKey = expirationKey(key);
+ var expr = getItem(exprKey);
+
+ if (expr) {
+ var expirationTime = parseInt(expr, EXPIRY_RADIX);
+
// Check if we should actually kick item out of storage
if (currentTime() >= expirationTime) {
- localStorage.removeItem(key);
- localStorage.removeItem(expirationKey(key));
- localStorage.removeItem(touchedKey(key));
+ removeItem(key);
+ removeItem(exprKey);
return null;
- } else {
- localStorage.setItem(touchedKey(key), currentTime());
- return parsedStorage(key);
}
- } else if (localStorage.getItem(key)) {
- localStorage.setItem(touchedKey(key), currentTime());
- return parsedStorage(key);
}
- return null;
+
+ // Tries to de-serialize stored value if its an object, and returns the normal value otherwise.
+ var value = 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;
+ }
},
/**
@@ -232,10 +236,48 @@ var lscache = function() {
* @param {string} key
*/
remove: function(key) {
- if (!supportsStorage) return null;
- localStorage.removeItem(key);
- localStorage.removeItem(expirationKey(key));
- localStorage.removeItem(touchedKey(key));
+ if (!supportsStorage()) return null;
+ removeItem(key);
+ removeItem(expirationKey(key));
+ },
+
+ /**
+ * Returns whether local storage is supported.
+ * Currently exposed for testing purposes.
+ * @return {boolean}
+ */
+ supported: function() {
+ return supportsStorage();
+ },
+
+ /**
+ * Flushes all lscache items and expiry markers without affecting rest of localStorage
+ */
+ flush: function() {
+ if (!supportsStorage()) return;
+
+ // Loop in reverse as removing items will change indices of tail
+ for (var i = localStorage.length-1; i >= 0 ; --i) {
+ var key = localStorage.key(i);
+ if (key.indexOf(CACHE_PREFIX + cacheBucket) === 0) {
+ localStorage.removeItem(key);
+ }
+ }
+ },
+
+ /**
+ * Appends CACHE_PREFIX so lscache will partition data in to different buckets.
+ * @param {string} bucket
+ */
+ setBucket: function(bucket) {
+ cacheBucket = bucket;
+ },
+
+ /**
+ * Resets the string being appended to CACHE_PREFIX so lscache will use the default storage behavior.
+ */
+ resetBucket: function() {
+ cacheBucket = '';
}
};
}();

0 comments on commit ce45216

Please sign in to comment.
Something went wrong with that request. Please try again.