If you have ever wanted an easy way to cache JavaScript objects and data in your client-side code just like you would when using the ASP.Net's Server side cache then read this article.
Monsur Hossain wrote a JavaScript LRU Cache which my partner found while we were searching for a JavaScript caching option. As I am a big fan of the ASP.Net AJAX framework and the fact that I was extremely impressed with the feature rich cache written by Monsur, which was fashioned after the ASP.Net's cache. I decided to port the code to an ASP.Net AJAX component.
ASP.Net AJAX extends JavaScript in some amazing ways which are beyond the scope of this article. However, ASP.Net AJAX does provide a framework to approach JavaScript in a more object oriented fashion. One such feature of the framework is building classes based on the prototype design pattern which is my goal in porting Monsur's code. You can find more information about the Least Recently Used caching algorithms at Wikipedia. And if you are not currently using ASP.Net AJAX or have no plan to implement it I invite you to visit Monsur's site for the original implementation to which a link can be found at the end of this article.
For those of you who do use ASP.Net AJAX and are looking for a JavaScript Object Cache read on as I describe the porting of the original which I call the JSOCache.
Start by creating a JSOCache.js file in your project.
JavaScript References, License Credit and Register Namespace
If you are developing with Visual Studio 2008 you will have the benefit of VS2008's JavaScript intelligence. Additionally for Licensing compliance and more importantly providing ample credit where it is due we will add the following to the beginning of our file JSOCache.js file. I have added to this section a shameless plug referencing the effort of porting this to ASP.Net AJAX. Lastly we will register the BehindTheCode.Web.UI namespace.
/// <reference name="MicrosoftAjax.js" />
/// <reference path="JSOCache.js" />
/*
MIT LICENSE
Copyright (c) 2007 Monsur Hossain (http://www.monsur.com)
Permission is hereby granted, free of charge, to any person
obtaining a copy of this software and associated documentation
files (the "Software"), to deal in the Software without
restriction, including without limitation the rights to use,
copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following
conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
OTHER DEALINGS IN THE SOFTWARE.
*/
/*
Ported to an ASP.Net AJAX component by Joseph Caudill (http://www.behindthecode.net)
*/
Type.registerNamespace('BehindTheCode.Web.UI');
Create Enumerations and supporting Classes
CachePriority Enumeration
This enumeration will be used to reference the priority of the cached item.
BehindTheCode.Web.UI.CachePriority = function(){};
BehindTheCode.Web.UI.CachePriority.prototype = {
/// <summary>
/// An easier way to refer to the priority of a cache item
/// Define an enumeration type and register it.
/// </summary>
Low: 1,
Normal: 2,
High: 4
}
BehindTheCode.Web.UI.CachePriority.registerEnum("BehindTheCode.Web.UI.CachePriority");
ItemExpireEventArgs Class
This event arguments class will be passed to the callback function of the CacheOptions when a cache item expires and a callback function has been provided.
BehindTheCode.Web.UI.ItemExpireEventArgs = function(key, value) {
/// <summary>Adds a event handler for the tick event.</summary>
/// <param name="key" type="Object">The key of the CacheItem that has expired.</param>
/// <param name="value" type="Object">The expired item.</param>
this._key = key;
this._value = value;
}
BehindTheCode.Web.UI.ItemExpireEventArgs.prototype = {
dispose: function() {
this._callback = null;
},
get_key : function() {
/// <value type="Object">The key of the CacheItem that has expired.</value>
return this._key;
},
set_key : function(value) {
this._key = value;
},
get_value : function() {
/// <value type="Object">The expired item.</value>
return this._value;
}
}
BehindTheCode.Web.UI.ItemExpireEventArgs.registerClass('BehindTheCode.Web.UI.ItemExpireEventArgs', Sys.EventArgs);
CacheOptions Class
This class manages priority and expiration options for a CacheItem as well as an expiration callback pointer.
BehindTheCode.Web.UI.CacheOptions = function(priority, expiration, expirationSliding, callback) {
/// <summary>Represents the options for a CacheItem</summary>
/// <param name="priority" type="BehindTheCode.Web.UI.CachePriority">
/// The priority to be given to the CacheItem.
/// How important it is to leave this item in the cache.
/// You can use the values CachePriority.Low, .Normal, or
/// .High, or you can just use an integer. Note that
/// placing a priority on an item does not guarantee
/// it will remain in cache. It can still be purged if
/// an expiration is hit, or if the cache is full.
/// </param>
/// <param name="expiration" type="Date">The datetime when the CacheItem should expire</param>
/// <param name="expiractionSliding" type="Number">
/// An integer representing the seconds since
/// the last cache access after which the item
/// should expire
/// </param>
/// <param name="callback" type="Function">
/// A function that gets called when the item is purged
/// from cache. An ItemExpireEventArgs is passed as parameters to the callback function.
/// </param>
if (priority == null) priority = BehindTheCode.Web.UI.CachePriority.Normal;
if (expiration != null) expiration = expiration.getTime();
this._priority = priority;
this._expirationAbsolute = expiration;
this._expirationSliding = expirationSliding;
this._callback = callback;
}
BehindTheCode.Web.UI.CacheOptions.prototype = {
dispose: function() {
this._callback = null;
},
get_priority : function() {
/// <value type="String">
/// key of cached item
/// </value>
return this._priority;
},
set_priority : function(value) {
this._priority = value;
},
get_expirationAbsolute : function() {
/// <value type="Number">
///
/// </value>
return this._expirationAbsolute;
},
set_expirationAbsolute : function(value) {
this._expirationAbsolute = value;
},
get_expirationSliding : function() {
/// <value type="Number">
/// An integer representing the seconds since
/// the last cache access after which the item
/// should expire
/// </value>
return this._expirationSliding;
},
set_expirationSliding : function(value) {
this._expirationSliding = value;
},
get_callback : function() {
/// <value type="Function">
/// A function that gets called when the item is purged
/// from cache. An ItemExpireEventArgs is passed as parameters to the callback function.
/// </value>
return this._callback;
},
set_callback : function(value) {
this._callback = value;
}
}
BehindTheCode.Web.UI.CacheOptions.registerClass('BehindTheCode.Web.UI.CacheOptions', null, Sys.IDisposable);
CacheItem Class
The CacheItem will represent an item in the cache. It will manage the key, cached object/value and optionally a CacheOptions defining how the JSOCache will manage the CacheItem.
BehindTheCode.Web.UI.CacheItem = function(key, value, options) {
/// <summary>The CacheItem will represent an item in the cache.</summary>
/// <param name="key" type="Object">Represents the unique key of the Cache Item</param>
/// <param name="value" type="Object">The object or value to be cached</param>
/// <param name="options" type="BehindTheCode.Web.UI.CacheOptions">The options by which the item will be managed</param>
// Validate the key
if ((key == null) || (value == ''))
throw new Error("key cannot be null or empty");
// Set the consturtor values
this._key = key;
this._value = value;
// Set a generic CacheOptions if one was not provided
if (options == null)
options = new BehindTheCode.Web.UI.CacheOptions(BehindTheCode.Web.UI.CachePriority.Normal, null, null, null);
this._options = options;
this._lastAccessed = new Date().getTime();
}
BehindTheCode.Web.UI.CacheItem.prototype = {
dispose: function() {
this._options.dispose();
},
get_key : function() {
/// <value type="Object">
/// key of cached item
/// </value>
return this._key;
},
set_key : function(value) {
this._key = value;
},
get_value : function() {
/// <value type="Object">
/// The cached object/value
/// </value>
return this._value;
},
set_value : function(value) {
this._value = value;
},
get_options : function() {
/// <value type="BehindTheCode.Web.UI.CacheOptions">
/// The cache options by which the cached item is managed
/// </value>
return this._options;
},
set_options : function(value) {
this._options = value;
},
get_lastAccessed : function() {
/// <value type="Date">
/// The date the item was lasted accessed from the cache
/// </value>
return this._lastAccessed;
},
set_lastAccessed : function(value) {
this._lastAccessed = value;
}
}
BehindTheCode.Web.UI.CacheItem.registerClass('BehindTheCode.Web.UI.CacheItem', null, Sys.IDisposable);
Creating the JSOCache Class
Finally we get to the JSOCache which is the caching component.
Constructor
In the constructor we will define and initialize our private fields. We will provide an optional parameter of the max size for the cache, and create the items array and statistics object.
BehindTheCode.Web.UI.JSOCache = function(maxSize) {
/// <summary>The JSOCache acts as a key/value cache which applies the LRU caching algorithm.</summary>
/// <param name="maxSize" type="Number">Optional. The maximum size of the cache. Default is -1 unlimited.</param>
BehindTheCode.Web.UI.JSOCache.initializeBase(this);
maxSize = maxSize == undefined ? -1 : maxSize;
// Properties
this._maxSize = maxSize;
this._fillFactor = .75;
this._count = 0;
this._stats = new Object();
this._stats.hits = 0;
this._stats.misses = 0;
// Variables
this._items = new Array();
this._purgeSize = Math.round(this._maxSize * this._fillFactor);
}
Start the Prototype and Define Public Methods
| setItem |
Sets an item in the cache |
| getItem |
Retrieves an item from the cache. Returns null if the item was not found |
| clear |
Remove all items from the cache |
| toHtmlString |
Generates HTML List of items in the cache and the statistics of the cache |
BehindTheCode.Web.UI.JSOCache.prototype = {
dispose : function() {
/// <summary>
/// Dispose of the componenet
/// </summary>
/// <returns />
this._items = null;
this._stats = null;
BehindTheCode.Web.UI.JSOCache.callBaseMethod(this, 'dispose');
},
getItem : function(key) {
/// <summary>
/// Retrieves an item from the cache.
/// </summary>
/// <param name="key" type="Object">
/// The key of the desired item in the cache
/// </param>
/// <returns>
/// Returns the matching item from the cache.
/// Returns null if the item doesn't exist
/// or it is expired.
/// </returns>
var item = this._items[key];
if (item != null) {
if (!this._isExpired(item)) {
// if the item is not expired
// update its last accessed date
item.set_lastAccessed(new Date().getTime());
} else {
// if the item is expired, remove it from the cache
this._removeItem(key);
item = null;
}
}
// return the item value (if it exists), or null
var returnVal = null;
if (item != null) {
returnVal = item.get_value();
this._stats.hits++;
} else {
this._stats.misses++;
}
return returnVal;
},
setItem : function(key, value, options) {
/// <summary>
/// Sets an item in the cache
/// </summary>
/// <param name="key" type="Object">
/// The unique key that will represent the object
/// </param>
/// <param name="value" type="Object">
/// The object to cache
/// </param>
/// <param name="options" type="BehindTheCode.Web.UI.CacheOptions">
/// The options by which the item will be managed.
/// </param>
/// <returns />
// add a new cache item to the cache
if (this._items[key] != null)
this._removeItem(key);
this._addItem(new BehindTheCode.Web.UI.CacheItem(key, value, options));
// if the cache is full, purge it
if ((this._maxSize > 0) && (this._count > this._maxSize)) {
this._purge();
}
},
clear : function() {
/// <summary>
/// Remove all items from the cache
/// </summary>
/// <returns />
// loop through each item in the cache and remove it
for (var key in this._items) {
this._removeItem(key);
}
},
toHtmlString : function() {
/// <summary>
/// Generates HTML List of items in the cache.
/// </summary>
/// <returns>
/// HTML
/// </returns>
var returnStr = this._count + " item(s) in cache<br /><ul>";
for (var key in this._items) {
var item = this._items[key];
try {
returnStr = returnStr + "<li>" + item.get_key().toString() + " = " + (item.get_value().toHtmlString != undefined ? item.get_value().toHtmlString() : item.get_value().toString()) + "</li>";
} catch(e) {
returnStr = returnStr + "<li>" + item.get_key().toString() + " = " + item.get_value().toString() + "</li>";
}
}
returnStr = returnStr + "</ul>";
returnStr = returnStr + "<br />";
returnStr = returnStr + "<br />";
returnStr = returnStr + "<b>Stats</b><br />";
returnStr = returnStr + "Hits = " + this._stats.hits + "<br />";
returnStr = returnStr + "Misses = " + this._stats.misses + "<br />";
return returnStr;
},
Define Private Methods
| addItem |
Add an item to the cache |
| removeItem |
Remove an item from the cache, call the callback function (if necessary) |
| isExpired |
Checks an items expiration state |
| purge |
Remove old elements from the cache based upon the associated options |
_purge : function() {
/// <summary>
/// Remove old elements from the cache based upon the associated options
/// </summary>
/// <returns />
var tmparray = new Array();
// loop through the cache, expire items that should be expired
// otherwise, add the item to an array
for (var key in this._items) {
var item = this._items[key];
if (this._isExpired(item)) {
this._removeItem(key);
} else {
tmparray.push(item);
}
}
if (tmparray.length > this._purgeSize) {
// sort this array based on cache priority and the last accessed date
tmparray = tmparray.sort(function(a, b) {
if (a.get_options().get_priority() != b.get_options().get_priority()) {
return b.get_options().get_priority() - a.get_options().get_priority();
} else {
return b.get_lastAccessed() - a.get_lastAccessed();
}
});
// remove items from the end of the array
while (tmparray.length > this._purgeSize) {
var ritem = tmparray.pop();
this._removeItem(ritem.get_key());
}
}
},
_addItem : function(item) {
/// <summary>
/// Add an item to the cache
/// </summary>
/// <param name="item" type="BehindTheCode.Web.UI.CacheItem">
/// The item to add to the cache
/// </param>
/// <returns />
this._items[item.get_key()] = item;
this._count++;
},
_removeItem : function(key) {
/// <summary>
/// Remove an item from the cache, call the callback function (if necessary)
/// </summary>
/// <param name="key" type="Object">
/// The key of the object to be removed
/// </param>
/// <returns />
var item = this._items[key];
delete this._items[key];
this._count--;
// if there is a callback function, call it at the end of execution
if (item.get_options().get_callback() != null) {
var callback = function() {
item.get_options().get_callback()(this, new BehindTheCode.Web.UI.ItemExpireEventArgs(item.get_key(), item.get_value()));
}
setTimeout(callback, 0);
}
},
_isExpired : function(item) {
/// <summary>
/// Checks an items expiration state
/// </summary>
/// <param name="item" type="Object">
/// The item to check
/// </param>
/// <returns>
/// Returns true if the item should be expired based on its expiration options
/// </returns>
var now = new Date().getTime();
var expired = false;
if ((item.get_options().get_expirationAbsolute()) && (item.get_options().get_expirationAbsolute() < now)) {
// if the absolute expiration has passed, expire the item
expired = true;
}
if ((expired == false) && (item.get_options().get_expirationSliding())) {
// if the sliding expiration has passed, expire the item
var lastAccess = item.get_lastAccessed() + (item.get_options().get_expirationSliding() * 1000);
if (lastAccess < now) {
expired = true;
}
}
return expired;
},
Define Properties
| maxSize |
The max size of the cache |
| fillFactor |
The factor to use when determining the size at which to purge items from the cache |
| count |
Gets the current count of items in the cache |
| stats |
Stats of the cache |
get_maxSize : function() {
/// <value type="Number" integer="true">
/// The max size of the cache
/// </value>
return this._maxSize;
},
set_maxSize : function(value) {
if (this._maxSize != value) {
this._maxSize = value;
this.raisePropertyChanged('MaxSize');
}
},
get_fillFactor : function() {
/// <value type="Number" integer="true">
/// The factor to use when determining the size at which to purge items from the cache
/// </value>
return this._fillFactor;
},
set_fillFactor : function(value) {
if (this._fillFactor != value) {
this._fillFactor = value;
this.raisePropertyChanged('FillFactor');
}
},
get_count : function() {
/// <value type="Number" integer="true">
/// Gets the current count of items in the cache
/// </value>
return this._count;
},
set_count : function(value) {
if (this._count != value) {
this._count = value;
this.raisePropertyChanged('Count');
}
},
get_stats : function() {
/// <value type="Object">
/// Stats of the cache
/// </value>
return this._stats;
},
set_stats : function(value) {
if (this._stats != value) {
this._stats = value;
this.raisePropertyChanged('Stats');
}
}
Close the Prototype, Register Class and Call Notify Script Loaded
}
BehindTheCode.Web.UI.JSOCache.registerClass('BehindTheCode.Web.UI.JSOCache', Sys.Component);
if (typeof(Sys) !== 'undefined') Sys.Application.notifyScriptLoaded();
Download the Code
JSOCache.js (17.85 kb) - ASP.NET AJAX Component
Monsur Hossain original implementation as Javascript LRU Cache
Be the first to rate this post
- Currently 0/5 Stars.
- 1
- 2
- 3
- 4
- 5