Friday, March 1, 2013

A deferred loading cache in Javascript

I was playing around with jQuery deferreds the other day, and thought up a neat use for them, in a client-side ajax response cache. I created a simple expiring cache abstraction, and then wrote a 'loading' cache around it, which populates the cache with $.Deferred instances which are resolved with the result of a $.get.

Here's the simple expiring cache:


steal(function() {
function ExpiringCache(options) {
if (!(this instanceof ExpiringCache)) {
return new ExpiringCache(options);
}
this._items = {};
this._size = 0;
this._defaultExpiry = options ? options.defaultExpiry : 60 * 1000;
}
ExpiringCache.prototype.get = function(key) {
var item = this._items[key];
if (item) {
if (new Date() - item.ctime >= item.expiry) {
delete this._items[key];
this._size--;
return null;
}
}
return item ? item.val : null;
};
ExpiringCache.prototype.put = function(key, val, expiry) {
var item = this.get(key);
this._items[key] = {
ctime: new Date().getTime(),
expiry: expiry || this._defaultExpiry,
val: val
}
if (!item) {
this._size++;
}
return item;
};
ExpiringCache.prototype.remove = function(key) {
var item = this.get(key);
if (item) {
delete this._items[key];
this._size--;
}
return item;
};
ExpiringCache.prototype.size = function() {
return this._size;
}
window.ExpiringCache = ExpiringCache;
})


And here's the deferred loading cache:


/*global ExpiringCache:false */
steal('jquery', 'expiring_cache.js', function() {
function LoadingCache() {
this.cache = new ExpiringCache();
}
LoadingCache.prototype.load = function(path) {
var item = this.cache.get(path);
if (!item) {
item = $.Deferred();
this.cache.put(path, item);
}
if (item.state() !== 'resolved') {
$.get(path, function(data) {
item.resolve(data);
});
}
return item;
};
window.LoadingCache = LoadingCache;
});


Jasmine spec for the loading cache:


/*global LoadingCache:false */
steal('app/util/loading_cache.js', function() {
describe("loading cache", function() {
var cache;
beforeEach(function() {
spyOn(jQuery, 'get');
cache = new LoadingCache();
})
it('returns deferred for non-existent entry', function() {
var item = cache.load('foo');
expect('resolve' in item).toBeTruthy();
expect(item.state()).not.toEqual('resolved');
})
it('invokes $.get with given path for unloaded entry', function() {
cache.load('foo');
expect(jQuery.get).toHaveBeenCalled();
expect(jQuery.get.calls[0].args[0]).toEqual('foo');
})
it('resolves the item deferred with the result of the $.get', function() {
var item = cache.load('foo'),
fn = jQuery.get.calls[0].args[1];
fn('bar');
expect(item.state()).toEqual('resolved');
item.done(function(val) {
expect(val).toEqual('bar');
})
})
it('doesnt call $.get again for resolved entry', function() {
cache.load('foo');
var fn = jQuery.get.calls[0].args[1];
fn('bar');
cache.load('foo');
expect(jQuery.get.calls.length).toBe(1);
})
})
})


In my next post, I'll show how to use a jasmine spec runner for steal to run jasmine steal specs in grunt.

No comments: