Skip to content

Instantly share code, notes, and snippets.

@grimen
Created May 31, 2012 12:43
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save grimen/2843157 to your computer and use it in GitHub Desktop.
Save grimen/2843157 to your computer and use it in GitHub Desktop.
node-http_cache
require('sugar');
var connect = require('connect');
var http_cache = {
http_cache_enabled: true,
http_cached: function(req, res, resource, callback) {
if (this.changed(req, res, resource)) {
if (Object.isFunction(callback)) {
callback(resource);
} else {
return resource;
}
} else {
// HTTP 304 "Not Modified" - response by: changed()
}
},
changed: function(req, res, resource) {
if (!this.http_cache_enabled)
return true;
var _this = this;
var last_modified;
if (this.is_collection(resource)) {
var last_resource = resource.map(function(r) { return _this.is_record(r); }).last();
last_modified = this.last_modified_at(last_resource)
} else {
last_modified = this.last_modified_at(resource);
}
var cache_options = {etag: resource};
if (last_modified) {
cache_options.last_modified = last_modified;
}
return this.stale(req, res, cache_options);
},
last_modified_at: function(resource, updated_at) {
updated_at = updated_at || 'updated_at';
if (this.is_record(resource, updated_at)) {
return (new Date(resource[updated_at])).toUTC();
} else {
return undefined;
}
},
is_record: function(resource, updated_at) {
updated_at = updated_at || 'updated_at';
return !!(Object.isObject(resource) && Object.has(resource, updated_at));
},
is_collection: function(resource) {
return Object.isArray(resource);
},
// ---------------------------------------------------
stale: function(req, res, options) {
var is_fresh = this.fresh_when(req, res, options);
return !is_fresh;
},
fresh_when: function(req, res, options) {
options = options || {};
if (options.etag)
this.set_etag(res, options.etag);
if (options.last_modified)
this.set_last_modified(res, options.last_modified);
var is_fresh = this.fresh(req, res);
if (is_fresh) {
connect.utils.notModified(res); // HTTP 304 "Not Modified"
} else {
// (nothing)
}
return is_fresh;
},
fresh: function(req, res) {
var check_last_modified = this.if_modified_since(req),
check_etag = this.if_none_match(req);
if (!check_last_modified && !check_etag)
return false;
var is_fresh = true;
if (check_last_modified)
is_fresh = is_fresh && this.not_modified(req, res.header('last-modified'));
if (check_etag)
is_fresh = is_fresh && this.etag_matches(req, res.header('etag'));
return is_fresh;
},
set_last_modified: function(res, last_modified, callback) {
last_modified = (new Date(last_modified)).toUTC().format(Date.RFC1036);
res.header('Last-Modified', last_modified);
},
set_etag: function(res, etag, callback) {
etag = connect.utils.md5(this.expand_cache_key(etag));
res.header('ETag', '"' + etag + '"');
},
expand_cache_key: function(key, namespace) {
var _this = this;
var expanded_cache_key = Object.isEmpty(namespace) ? '' : namespace + '/',
prefix = process.env.HTTP_CACHE_ID;
if (!Object.isEmpty(prefix)) {
expanded_cache_key = expanded_cache_key + prefix + '/';
}
if (key && key.cache_key) {
key = Object.isFunction(key.cache_key) ? key.cache_key() : key.cache_key;
} else if (key && Object.isObject(key)) {
key = connect.utils.md5(JSON.stringify(key));
} else if (key && Object.isArray(key)) {
key = key.compact().map (function(v) { return _this.expand_cache_key(v); }).join('-');
}
if (key) {
key = key.toString()
expanded_cache_key = expanded_cache_key + key;
}
return expanded_cache_key;
},
not_modified: function(req, modified_at) {
var modified_since = this.if_modified_since(req);
modified_at = (new Date(modified_at)).toUTC().format('{yyyy}-{MM}-{dd} {hh}:{mm}:{ss} UTC');
return !!(modified_since && modified_at && modified_since >= modified_at);
},
modified: function(req, modified_at) {
return !this.not_modified(req, modified_at);
},
etag_matches: function(req, etag) {
var none_match = this.if_none_match(req);
return !!(Object.isString(none_match) && none_match.replace(/\"/g, '') === etag.replace(/\"/g, ''));
},
if_modified_since: function(req) {
var modified_since = req.headers['if-modified-since'];
if (modified_since) {
return (new Date(modified_since)).toUTC().format('{yyyy}-{MM}-{dd} {hh}:{mm}:{ss} UTC');
} else {
return null;
}
},
if_none_match: function(req) {
return req.headers['if-none-match'];
}
};
module.exports = http_cache;
var vows = require('vows'),
assert = require('assert'),
sugar = require('sugar'),
connect = require('connect'),
http = require('http'),
helper = require('../spec_helper.js'),
http_cache = require('../../lib/util/http_cache');
vows.describe('util.http_cache')
.addBatch({
'.http_cached': {
// TODO: Annoying to test currently, should port expresso assert.request to test middleware.
},
'.changed': {
// TODO: See above.
},
'.last_modified_at': {
"undefined": function() {
assert.equal ( http_cache.last_modified_at(undefined), undefined );
},
"null": function() {
assert.equal ( http_cache.last_modified_at(null), null );
},
"Object: {foo: 'bar'}": function() {
assert.equal ( http_cache.last_modified_at({foo: 'bar'}), null );
},
"Object: {foo: 'bar', updated_at: '2012-01-01 12:00:00 UTC'}": function() {
assert.deepEqual ( http_cache.last_modified_at({foo: 'bar', updated_at: '2012-01-01 12:00:00 UTC'}), new Date('Sun, 01 Jan 2012 11:00:00 GMT') );
},
"Object: {foo: 'bar', updated_at: new Date('2012-01-01 12:00:00 UTC')}": function() {
assert.deepEqual ( http_cache.last_modified_at({foo: 'bar', updated_at: new Date('2012-01-01 12:00:00 UTC')}), new Date('Sun, 01 Jan 2012 11:00:00 GMT') );
}
},
'.is_record': {
"undefined": function() {
assert.equal ( http_cache.is_record(undefined), false );
},
"null": function() {
assert.equal ( http_cache.is_record(null), false );
},
"Object: {foo: 'bar'}": function() {
assert.equal ( http_cache.is_record({foo: 'bar'}), false );
},
"Object: {foo: 'bar', updated_at: '2012-01-01 12:00:00 UTC'}": function() {
assert.equal ( http_cache.is_record({foo: 'bar', updated_at: '2012-01-01 12:00:00 UTC'}), true );
},
"Object: {foo: 'bar', updated_at: new Date('2012-01-01 12:00:00 UTC')}": function() {
assert.equal ( http_cache.is_record({foo: 'bar', updated_at: new Date('2012-01-01 12:00:00 UTC')}), true );
}
},
'.is_collection': {
"undefined": function() {
assert.equal ( http_cache.is_collection(undefined), false );
},
"null": function() {
assert.equal ( http_cache.is_collection(null), false );
},
"String": function() {
assert.equal ( http_cache.is_collection("Foo bar"), false );
},
"Object": function() {
assert.equal ( http_cache.is_collection({}), false );
},
"Array": function() {
assert.equal ( http_cache.is_collection([]), true );
}
},
// ------------------------
'.stale': {
// TODO: Annoying to test currently, should port expresso assert.request to test middleware.
},
'.fresh_when': {
// TODO: See above.
},
'.fresh': {
// TODO: See above.
},
'.set_last_modified': {
// TODO: See above.
},
'.set_etag': {
// TODO: See above.
},
'.expand_cache_key': {
"undefined": function() {
assert.equal ( http_cache.expand_cache_key(undefined), "" );
},
"null": function() {
assert.equal ( http_cache.expand_cache_key(null), "" );
},
"String: ''": function() {
assert.equal ( http_cache.expand_cache_key(''), "" );
},
"String: '123'": function() {
assert.equal ( http_cache.expand_cache_key('123'), "123" );
},
"Array: ['1', '2', '3']": function() {
assert.equal ( http_cache.expand_cache_key(['1', '2', '3']), "1-2-3" );
},
"Array: [1, 2, 3]": function() {
assert.equal ( http_cache.expand_cache_key([1, 2, 3]), "1-2-3" );
},
"Array: ['1', undefined, '3']": function() {
assert.equal ( http_cache.expand_cache_key([1, undefined, 3]), "1-3" );
},
"Array: ['1', null, '3']": function() {
assert.equal ( http_cache.expand_cache_key([1, null, 3]), "1-3" );
},
"Object: {foo: 'bar'}": function() {
assert.equal ( http_cache.expand_cache_key({foo: 'bar'}), connect.utils.md5(JSON.stringify({foo: 'bar'})) );
},
"Object: {foo: 'bar', cache_key: '3-2-1'}": function() {
assert.equal ( http_cache.expand_cache_key({foo: 'bar', cache_key: '3-2-1'}), "3-2-1" );
}
},
'.not_modified': {
"'If-Modified-Since' not set": function() {
var req = {
headers: {}
};
assert.equal ( http_cache.not_modified(req, 'Mon, 1 Jan 2012 12:00:01 GMT'), false );
},
"'If-Modified-Since' set: <non-expired>": function() {
var req = {
headers: {'if-modified-since': 'Mon, 1 Jan 2012 12:00:00 GMT'}
};
assert.equal ( http_cache.not_modified(req, 'Mon, 1 Jan 2012 11:59:59 GMT'), true );
},
"'If-Modified-Since' set: <equal>": function() {
var req = {
headers: {'if-modified-since': 'Mon, 1 Jan 2012 12:00:00 GMT'}
};
assert.equal ( http_cache.not_modified(req, 'Mon, 1 Jan 2012 12:00:00 GMT'), true );
},
"'If-Modified-Since' set: <expired>": function() {
var req = {
headers: {'if-modified-since': 'Mon, 1 Jan 2012 12:00:00 GMT'}
};
assert.equal ( http_cache.not_modified(req, 'Mon, 1 Jan 2012 12:00:01 GMT'), false );
}
},
'.etag_matches': {
"'If-None-Match' not set": function() {
var req = {
headers: {}
};
assert.equal ( http_cache.etag_matches(req, '13fca1035d7083e4fd2403c9cc36ab8a2d79518e'), false );
},
"'If-None-Match' set: <equal>": function() {
var req = {
headers: {'if-none-match': '"13fca1035d7083e4fd2403c9cc36ab8a2d79518e"'}
};
assert.equal ( http_cache.etag_matches(req, '13fca1035d7083e4fd2403c9cc36ab8a2d79518e'), true );
},
"'If-None-Match' set: <non-equal>": function() {
var req = {
headers: {'if-none-match': '"13fca1035d7083e4fd2403c9cc36ab8a2d79518e"'}
};
assert.equal ( http_cache.etag_matches(req, 'a3fca1035d7083e4fd2403c9cc36ab8a2d79518e'), false );
}
},
'.if_modified_since': {
"'If-Modified-Since' not set": function() {
var req = {
headers: {}
};
assert.equal ( http_cache.if_modified_since(req), null );
},
"'If-Modified-Since' set": function() {
var req = {
headers: {'if-modified-since': 'Mon, 1 Jan 2012 12:00:00 GMT'}
};
assert.equal ( http_cache.if_modified_since(req), '2012-01-01 12:00:00 UTC' );
}
},
'.if_none_match': {
"'If-None-Match' not set": function() {
var req = {
headers: {}
};
assert.equal ( http_cache.if_none_match(req), null );
},
"'If-None-Match' set": function() {
var req = {
headers: {'if-none-match': '"13fca1035d7083e4fd2403c9cc36ab8a2d79518e"'}
};
assert.equal ( http_cache.if_none_match(req), '"13fca1035d7083e4fd2403c9cc36ab8a2d79518e"' );
},
}
}).export(module);
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment