local.redis.js | |
---|---|
Authors: Joel Kemp and Eudis Duran File: local.redis.js Purpose: Replicates the Redis API for use with HTML5 Storage Objects Usage: window.localRedis along with any supported commands. | (function (window, document, undefined) {
"use strict";
if (! window.localStorage) {
Object.defineProperty(window, "localStorage", (function () {
var aKeys = [], oStorage = {};
Object.defineProperty(oStorage, "getItem", {
value: function (sKey) { return sKey ? this[sKey] : null; },
writable: false,
configurable: false,
enumerable: false
});
Object.defineProperty(oStorage, "key", {
value: function (nKeyId) { return aKeys[nKeyId]; },
writable: false,
configurable: false,
enumerable: false
});
Object.defineProperty(oStorage, "setItem", {
value: function (sKey, sValue) {
if(!sKey) { return; }
document.cookie = window.escape(sKey) + "=" + window.escape(sValue) + "; expires=Tue, 19 Jan 2038 03:14:07 GMT; path=/";
},
writable: false,
configurable: false,
enumerable: false
});
Object.defineProperty(oStorage, "length", {
get: function () { return aKeys.length; },
configurable: false,
enumerable: false
});
Object.defineProperty(oStorage, "removeItem", {
value: function (sKey) {
if(!sKey) { return; }
document.cookie = window.escape(sKey) + "=; expires=Thu, 01 Jan 1970 00:00:00 GMT; path=/";
},
writable: false,
configurable: false,
enumerable: false
});
this.get = function () {
var iThisIndx, sKey, aCouple, aCouples, iKey, nIdx;
for (sKey in oStorage) {
iThisIndx = aKeys.indexOf(sKey);
if (iThisIndx === -1) { oStorage.setItem(sKey, oStorage[sKey]); }
else { aKeys.splice(iThisIndx, 1); }
delete oStorage[sKey];
}
for (aKeys; aKeys.length > 0; aKeys.splice(0, 1)) { oStorage.removeItem(aKeys[0]); }
for (aCouple, iKey, nIdx = 0, aCouples = document.cookie.split(/\s*;\s*/); nIdx < aCouples.length; nIdx++) {
aCouple = aCouples[nIdx].split(/\s*=\s*/);
if (aCouple.length > 1) {
oStorage[iKey = window.unescape(aCouple[0])] = window.unescape(aCouple[1]);
aKeys.push(iKey);
}
}
return oStorage;
};
this.configurable = false;
this.enumerable = true;
})());
}
var JSON = {
parse:
window.JSON && (window.JSON.parse || window.JSON.decode) ||
String.prototype.evalJSON && function(str){return String(str).evalJSON();},
stringify:
Object.toJSON ||
window.JSON && (window.JSON.stringify || window.JSON.encode)
}; |
Break if no JSON support was found | if(! (JSON.parse && JSON.stringify)){
throw new Error('No JSON support found');
}
var storage = window.localStorage,
localRedis = {}; |
///////////////////////// Utilities ///////////////////////// | var
isString = function (element) {
return typeof element === 'string';
},
isNumber = function (element) {
return typeof element === 'number';
},
stringified = function (element) {
return isString(element) ? element : JSON.stringify(element);
},
parsed = function (element) {
try { |
If it's a literal string, parsing will fail | element = JSON.parse(element);
} catch (e) { |
We couldn't parse the literal string so just return the literal in the finally block. | } finally {
return element;
}
}, |
Used to emit cross-browser events Note: Checks for IE support, then Jquery, then other browsers | eventName = 'storagechange',
fireEvent = function (element, event){
var evt,
$ = window.$ || window.jQuery; |
dispatch for IE | if (document.createEventObject){
evt = document.createEventObject();
return element.fireEvent('on' + event, evt); |
If jQuery exists, dispatch with jquery | } else if($) {
$(element).trigger(event); |
dispatch for firefox + others | } else{
evt = document.createEvent("HTMLEvents"); |
event type, bubbling, cancelable | evt.initEvent(event, true, true);
return !element.dispatchEvent(evt);
}
}, |
Fired when the storage changes Attaches events to the document to avoid | fireStorageChangedEvent = function () {
return fireEvent(document, 'storagechange');
}; |
///////////////////////// Error Helper TODO: MOVE TO EXTERNAL PLUGIN ///////////////////////// | var
errors = [
'wrong number of arguments',
'non-string value',
'value is not an integer or out of range',
'not a string value',
'timestamp already passed',
'delay not convertible to a number',
'source and destination objects are the same',
'no such key',
'missing storage context'
],
generateError = function (type /*, functionName, errorType */) {
var error,
message,
functionName = arguments[1],
errorType = arguments[2];
if (typeof type !== 'number' || (functionName && typeof functionName !== 'string')) {
throw new TypeError('wrong arg types');
}
message = errors[type];
if (errorType) {
errorType = errorType.toLowerCase();
if (errorType === 'typeerror') {
error = new TypeError(message);
}
} else {
error = new Error(message);
}
return error;
}; |
///////////////////////// Expiration Internals ///////////////////////// | var |
Creates and returns the expiration key format for a given storage key | createExpirationKey = function (key) {
var delimiter = ":",
prefix = "e";
key = stringified(key);
return prefix + delimiter + key;
}, |
Creates the expiration value/data format for an expiration event's ID and millisecond delay Returns: A string representation of an object created from the data Note: Keys are as short as possible to save space when stored | createExpirationValue = function (delay, currentTime) {
return stringified({
c: currentTime,
d: delay
});
}, |
Retrieves the parsed expiration data for the storage key | getExpirationValue = function (key) {
var expKey = createExpirationKey(key),
expVal = storage.getItem(expKey);
return parsed(expVal);
}, |
Retrieves the expiration delay of the key | getExpirationDelay = function (key) {
var expVal = getExpirationValue(key);
return (expVal && expVal.d) ? expVal.d : null;
}, |
Returns the expiration's creation time in ms | getExpirationCreationTime = function (key) {
var expVal = getExpirationValue(key);
return (expVal && expVal.c) ? expVal.c : null;
}, |
Returns the time remaining (in ms) for the expiration | getExpirationTTL = function (key) {
var expVal = getExpirationValue(key),
ttl;
if (expVal && expVal.d && expVal.c) { |
TTL is the difference between the creation time w/ delay and now | ttl = (expVal.c + expVal.d) - new Date().getTime();
}
return ttl;
}, |
Stores expiration data for the passed key | setExpirationOf = function (key, delay, currentTime) {
var expKey = createExpirationKey(key),
expVal = createExpirationValue(delay, currentTime);
storage.setItem(expKey, expVal);
}, |
Removes/Cancels the key's expiration | removeExpirationOf = function (key) {
var expKey = createExpirationKey(key),
expVal = getExpirationValue(key);
storage.removeItem(expKey);
}, |
Whether or not the given key has existing expiration data Returns: true if expiry data exists, false otherwise | hasExpiration = function (key) {
var expKey = createExpirationKey(key);
return !! storage.getItem(expKey);
}, |
Whether or not the key's ttl indicates that it should be removed | shouldExpire = function (key) {
var ttl = getExpirationTTL(key),
shouldExpire = ttl < 0;
return shouldExpire;
}, |
Removes a key and its expiration data if the key should expire Note: Each process is responsible for cleaning out expired keys | cleanIfExpired = function (key) {
if (shouldExpire(key)) {
remove(key);
remove(createExpirationKey(key));
}
}; |
///////////////////////// Native Storage Methods ///////////////////////// | localRedis.setItem = function (key, value) {
storage.setItem(key, value);
fireStorageChangedEvent();
},
localRedis.getItem = function (key) { |
Have to clean otherwise we have an expiration loophole | cleanIfExpired(key);
return storage.getItem(key);
},
localRedis.key = function (index) {
return storage.key(index);
};
localRedis.clear = function () {
storage.clear();
fireStorageChangedEvent();
};
localRedis.removeItem = function (key) {
storage.removeItem(key);
fireStorageChangedEvent();
}; |
///////////////////////// Storage Internals ///////////////////////// | |
Redis commands typically have side effects and so we should be cautious to include calls to those functions when requiring storage operations with no side effects. These internal functions are safer to use for storage within commands | var |
Stores the key/value pair Note: Auto-stringifies non-strings Throws: Exception on reaching the storage quota | store = function (key, value) {
key = stringified(key);
value = stringified(value);
try {
storage.setItem(key, value);
fireStorageChangedEvent();
} catch (e) { |
Quota exception | throw e;
}
}, |
Returns the (parsed) value associated with the given key Note: to ensure that expired keys are removed, we have to do it in core retrieval that all other commands use | retrieve = function (key) {
key = stringified(key);
cleanIfExpired(key);
var res = storage.getItem(key);
return parsed(res);
}, |
Remove the key/value pair identified by the passed key Note: Auto stringifies non-strings | remove = function (key) {
key = stringified(key);
storage.removeItem(key);
}, |
Returns true if the key(s) exists, false otherwise. Notes: A key with a set value of null still exists. Usage: exists('foo') or exists(['foo', 'bar']) | exists = function (key) {
cleanIfExpired(key);
var allExist = true,
i, l;
if (key instanceof Array) {
for (i = 0, l = key.length; i < l; i++) {
if (! storage.hasOwnProperty(key[i])) {
allExist = false;
}
}
} else { |
localRedis object doesn't hold key/value pairs | allExist = !! storage.hasOwnProperty(key);
}
return allExist;
}; |
///////////////////////// Keys Commands ///////////////////////// | |
Removes the specified key(s) Returns: the number of keys removed. Notes: if the key doesn't exist, it's ignored. clears existing expirations on the keys Usage: del('k1') or del('k1', 'k2') or del(['k1', 'k2']) | localRedis.del = function (keys) {
var numKeysDeleted = 0,
i, l;
keys = (keys instanceof Array) ? keys : arguments;
for (i = 0, l = keys.length; i < l; i++) {
if (exists(keys[i])) {
removeExpirationOf(keys[i]);
remove(keys[i]);
++numKeysDeleted;
}
}
return numKeysDeleted;
}; |
Returns: 1 if the key exists, 0 if they key doesn't exist. Throws: TypeError if more than one argument is supplied | localRedis.exists = function (key) {
if (arguments.length > 1) throw generateError(0);
return exists(key) ? 1 : 0;
}; |
Renames key to newkey Throws: Error if key == newkey Error if key does not exist Usage: rename(key, newkey) Notes: Transfers the key's TTL to the newKey | localRedis.rename = function (key, newKey) {
var errorType, ttl;
if (arguments.length !== 2) {
errorType = 0;
} else if (key === newKey) {
errorType = 6;
} else if (! exists(key)) {
errorType = 7;
} |
errorType could be 0, so don't do if (errorType) | if (errorType !== undefined) throw generateError(errorType); |
Remove newKey's existing expiration since newKey inherits all characteristics from key | if (hasExpiration(newKey)) removeExpirationOf(newKey); |
Assign the value of key to newKey | store(newKey, retrieve(key)); |
Transfer an existing expiration to newKey | if (hasExpiration(key)) {
ttl = getExpirationTTL(key); |
Transfer the TTL (ms) to the new key | this.pexpire(newKey, ttl); |
Remove the old key's expiration | removeExpirationOf(key);
}
remove(key);
}; |
Renames key to newkey if newkey does not exist Returns: 1 if key was renamed; 0 if newkey already exists Usage: renamenx(key, newkey) Notes: Affects expiry like rename Throws: TypeError if key == newkey ReferenceError if key does not exist Fails under the same conditions as rename | localRedis.renamenx = function (key, newKey) {
var typeError;
if (arguments.length !== 2) {
typeError = 0;
} else if (key === newKey) {
typeError = 6;
} else if (! exists(key)) {
typeError = 7;
}
if (typeError) throw generateError(typeError);
if (exists(newKey)) return 0; |
Rename and transfer expirations | this.rename(key, newKey);
return 1;
}; |
Retrieves the first key associated with the passed value Returns: a single key or a list of keys if true is passed as second param or null if no keys were found Params: all = whether or not to retrieve all of the keys that match Notes: Custom, non-redis method | localRedis.getkey = function (val) {
if (arguments.length > 2) throw generateError(0);
var i, l, k, v, keys = [], all; |
Get whether or not the all flag was set | all = !! arguments[1]; |
Look for keys with a value that matches val | for (i = 0, l = storage.length; i < l; i++) {
k = storage.key(i);
v = storage.getItem(k);
if (val === v) {
keys.push(k);
if (! all) break;
}
} |
Return the single element or null if undefined Otherwise, return the populated array | if (keys.length === 1) {
keys = keys[0];
} else if (! keys.length) {
keys = null;
}
return keys;
}; |
expire Expires the passed key after the passed seconds Precond: delay in seconds Returns: 1 if the timeout was set 0 if the key does not exist or the timeout couldn't be set | localRedis.expire = function (key, delay) {
if (arguments.length !== 2) throw generateError(0);
if (! exists(key)) return 0; |
Check if the delay is/contains a number | delay = parseFloat(delay, 10);
if (! delay) throw generateError(5); |
Convert the delay to ms (1000ms in 1s) | delay *= 1000; |
Subsequent calls to expire on the same key will refresh the expiration with the new delay | if (hasExpiration(key)) removeExpirationOf(key); |
Create the key's new expiration data | setExpirationOf(key, delay, new Date().getTime());
return 1;
}; |
Whether or not the key is going to expire Returns: 1 if the key has expiration data 0 if the key does not have expiration data Notes: Custom function | localRedis.expires = function (key) {
key = stringified(key);
return hasExpiration(key) ? 1 : 0;
}; |
Expiry in milliseconds Returns: the same output as expire | localRedis.pexpire = function (key, delay) {
if (arguments.length !== 2) throw generateError(0); |
Check if the delay is/contains a number | delay = parseFloat(delay, 10);
if (! delay) throw generateError(5); |
Expire will convert the delay to seconds, so we account for that by canceling out the conversion from ms to s | return this.expire(key, delay / 1000);
}; |
Expires a key at the supplied, second-based UNIX timestamp Returns: 1 if the timeout was set. 0 if key does not exist or the timeout could not be set Usage: expireat('foo', 1293840000) | localRedis.expireat = function (key, timestamp) {
if (arguments.length !== 2) throw generateError(0); |
Compute the delay (in seconds) | var nowSeconds = new Date().getTime() / 1000,
delay = timestamp - nowSeconds;
if (delay < 0) throw generateError(4);
return this.expire(key, delay);
}; |
Expires a key a the supplied, millisecond-based UNIX timestamp Returns: 1 if the timeout was set. 0 if key does not exist or the timeout could not be set | localRedis.pexpireat = function (key, timestamp) {
if (arguments.length !== 2) throw generateError(0); |
Delay in milliseconds | var delay = timestamp - new Date().getTime();
if(delay < 0) throw generateError(4);
return this.pexpire(key, delay);
}; |
Removes the expiration associated with the key Returns: 0 if the key does not exist or does not have an expiration 1 if the expiration was removed | localRedis.persist = function (key) {
if (arguments.length !== 1) throw generateError(0);
if (! (exists(key) && hasExpiration(key))) return 0;
remove(createExpirationKey(key));
return 1;
}; |
Returns: the time to live in seconds -1 when key does not exist or does not have an expiration Notes: Due to the possible delay between expiration timeout firing and the callback execution, this ttl only reflects the TTL for the timeout firing | localRedis.ttl = function (key) {
if (arguments.length !== 1) throw generateError(0);
if(exists(key) && hasExpiration(key)) { |
1sec = 1000ms | return getExpirationTTL(key) / 1000;
}
return -1;
}; |
Returns: the time to live in milliseconds -1 when key does not exist or does not have an expiration Note: this command is just like ttl with ms units | localRedis.pttl = function (key) {
if (arguments.length !== 1) throw generateError(0);
return this.ttl(key) * 1000;
}; |
Returns: a random key from the calling storage object. null when the database is empty | localRedis.randomkey = function () {
var keys = Object.keys(storage),
length = storage.length, |
Random position within the list of keys | rindex = Math.floor(Math.random() * length);
if (! length) return null;
return keys[rindex];
}; |
Returns: all keys matching the supplied pattern null if no keys were found Usage: keys('foo*') for all keys with foo | localRedis.keys = function (pattern) {
var regex = new RegExp(pattern),
i, l,
results = [],
keys = Object.keys(storage);
for (i = 0, l = keys.length; i < l; i++) {
if (regex.test(keys[i])) {
results.push(keys[i]);
}
}
return results.length ? results : null;
}; |
///////////////////////// String Commands ///////////////////////// | |
Returns: The (parsed) value associated with the passed key, if it exists. | localRedis.get = function(key) {
return retrieve(key);
}; |
Stores the passed value indexed by the passed key Notes: Auto stringifies resets an existing expiration if set was called directly Usage: set('foo', 'bar') or set('foo', 'bar').set('bar', 'car') | localRedis.set = function(key, value) {
var hasExp = hasExpiration(key),
expDelay;
try {
store(key, value); |
Cancel the expiration of the key | if (hasExp) {
expDelay = getExpirationDelay(key);
this.persist(key);
}
} catch (e) {
throw e;
} |
Makes chainable | return this;
}; |
Sets key to value and returns the old value stored at key Throws: Error when key exists but does not hold a string value Usage: getset(key, value) Notes: Removes an existing expiration for key Returns: the old value stored at key or null when the key does not exist | localRedis.getset = function (key, value) {
if (arguments.length !== 2) throw generateError(0); |
Grab the existing value or null if the key doesn't exist | var oldVal = retrieve(key); |
Throw an exception if the value isn't a string | if (! isString(oldVal) && oldVal !== null) throw generateError(1); |
Use set to refresh an existing expiration | this.set(key, value);
return oldVal;
}; |
Returns: A list of values for the passed key(s). Note: Values match keys by index. Usage: mget('key1', 'key2', 'key3') or mget(['key1', 'key2', 'key3']) | localRedis.mget = function(keys) {
var results = [],
i, l; |
Determine the form of the parameters | keys = (keys instanceof Array) ? keys : arguments; |
Retrieve the value for each key | for (i = 0, l = keys.length; i < l; i++) {
results[results.length] = retrieve(keys[i]);
}
return results;
}; |
Allows the setting of multiple key value pairs Usage: mset('key1', 'val1', 'key2', 'val2') or mset(['key1', 'val1', 'key2', 'val2']) or mset({key1: val1, key2: val2}) Notes: If there's an odd number of elements, unset values default to undefined. | localRedis.mset = function (keysVals) {
var isArray = keysVals instanceof Array,
isObject = keysVals instanceof Object,
i, l,
keys; |
Arrays are both an array and an object but an object is solely an object | if (isObject && !isArray) {
keys = Object.keys(keysVals);
for (i = 0, l = keys.length; i < l; i++) {
store(keys[i], keysVals[keys[i]]);
}
} else {
keysVals = isArray ? keysVals : arguments;
for (i = 0, l = keysVals.length; i < l; i += 2) {
store(keysVals[i], keysVals[i + 1]);
}
}
return this;
}; |
Set key to hold string value if key does not exist. Returns: 1 if the key was set 0 if the key was not set Note: When key already holds a value, no operation is performed. | localRedis.setnx = function (key, value) {
if(arguments.length !== 2) throw generateError(0);
if (exists(key)) return 0;
store(key, value);
return 1;
}; |
Sets the given keys to their respective values. Returns: 1 if the all the keys were set. 0 if no key was set (at least one key already existed). Notes: Accepts the same types of params as mset. If just a single key already exists, no set operations are performed | localRedis.msetnx = function (keysVals) {
var isArray = keysVals instanceof Array,
isObject = keysVals instanceof Object,
i, l,
keys = []; |
Grab all of the keys | if (isObject && ! isArray) {
keys = Object.keys(keysVals);
} else {
keysVals = isArray ? keysVals : arguments;
for (i = 0, l = keysVals.length; i < l; i += 2) {
keys.push(keysVals[i]);
}
} |
If any key exists, then don't store anything | for (i = 0, l = keys.length; i < l; i++) {
if (exists(keys[i])) {
return 0;
}
} |
We don't call mset because splat arguments are passed in as an object, when it should be processed like an array | for (i = 0, l = keysVals.length; i < l; i += 2) {
store(keysVals[i], keysVals[i + 1]);
}
return 1;
}; |
If the key does not exist, incr sets it to 1 Note: incr does not affect expiry | localRedis.incr = function (key) {
if (arguments.length !== 1) throw generateError(0);
this.incrby(key, 1);
}; |
If the key does not exist, incrby sets it to amount Notes: Incrby does not affect key expiry keys set with null values cannot be incremented amount must be a number or string containing a number Usage: incrby('foo', '4') or incrby('foo', 4) | localRedis.incrby = function (key, amount) {
if (arguments.length !== 2) throw generateError(0);
var value = retrieve(key),
valType = typeof value,
amountType = typeof amount,
parsedValue = parseInt(value, 10),
parsedAmount = parseInt(amount, 10),
amountIsNaN = isNaN(parsedAmount),
valueIsNaN = isNaN(parsedValue), |
Check the value | isValNull = value === null,
isValNumber = isNumber(valType),
isValNumberStr = isString(valType) && !valueIsNaN,
isValNotNumberStr = isString(valType) && valueIsNaN, |
Value should be a number or string representation of a number Example values: 1 or "1" | isValNotValid = !isValNumber && isValNotNumberStr, |
Key exists with a value of null | existsNullVal = valueIsNaN && exists(key), |
Check the amount | isAmountNumber = isNumber(amountType),
isAmountNumberStr = isString(amountType) && !amountIsNaN,
isAmountNotNumberStr = isString(amountType) && amountIsNaN,
isAmountNotValid = !isAmountNumber && isAmountNotNumberStr, |
Out of range checks | valOutOfRange = !valueIsNaN && (value >= Number.MAX_VALUE),
amountOutOfRange = !amountIsNaN && (amount >= Number.MAX_VALUE),
anyOutOfRange = valOutOfRange || amountOutOfRange;
if ((isValNotValid && !isValNull) || existsNullVal || amountIsNaN || isAmountNotValid || anyOutOfRange) { |
out of range or not an integer | throw generateError(2); |
The value and incr amount are valid | } else if ((isValNumber || isValNumberStr) && (isAmountNumber || isAmountNumberStr)) {
value = parsedValue + parsedAmount; |
Key didn't exist, so set the value to the amount | } else if (isAmountNumber || isAmountNumberStr) {
value = parsedAmount;
}
store(key, value);
}; |
Increments multiple keys by 1 | localRedis.mincr = function (keys) {
var i, l;
keys = (keys instanceof Array) ? keys : arguments;
for(i = 0, l = keys.length; i < l; i++) {
this.incr(keys[i]);
}
}; |
Usage: mincrby('key1', 1, 'key2', 4) or mincrby(['key1', 1, 'key2', 2]) or mincrby({'key1': 1, 'key2': 2}) Notes: Custom, non-redis method | localRedis.mincrby = function (keysAmounts) {
var i, l, key;
if (keysAmounts instanceof Array || isString(keysAmounts)) {
keysAmounts = (keysAmounts instanceof Array) ? keysAmounts : arguments; |
Need to make sure an even number of arguments is passed in | if ((keysAmounts.length & 0x1) !== 0) throw generateError(0);
for (i = 0, l = keysAmounts.length; i < l; i += 2) {
this.incrby(keysAmounts[i], keysAmounts[i + 1]);
}
} else if (keysAmounts instanceof Object) {
for (key in keysAmounts) {
this.incrby(key, keysAmounts[key]);
}
}
}; |
decr | |
decrby | |
mdecrby | |
Appends the value at the end of the string Returns: the length of the string after appending the original length for non-string values Notes: Appends if the key exists and is a string If key does not exist, we initialize it to empty and perform the append | localRedis.append = function (key, value) {
if (arguments.length !== 2) throw generateError(0);
var val = retrieve(key) || "",
valIsString = isString(val);
if (valIsString) {
val += value;
store(key, val);
}
return valIsString ? val.length : 1;
}; |
Returns: the length of the string value stored at key. 0 when key does not exist Throws: when the key holds a non-string value | localRedis.strlen = function (key) {
var val = retrieve(key);
if (! val) return 0;
if (! isString(val)) throw generateError(1);
return val.length;
}; |
Set key to hold the string value and set key to timeout after a given number of seconds. | localRedis.setex = function (key, value, delay) {
if (arguments.length !== 3) throw generateError(0);
store(key, value);
this.expire(key, delay);
}; |
Set key to hold the string value and set key to timeout after a given number of milliseconds. | localRedis.psetex = function (key, value, delay) {
if (arguments.length !== 3) throw generateError(0);
store(key, value);
this.pexpire(key, delay);
};
window.localRedis = localRedis;
})(window, document);
|