|
|
/*! * Connect - staticCache * Copyright(c) 2011 Sencha Inc. * MIT Licensed */
/** * Module dependencies. */
var utils = require('../utils') , Cache = require('../cache') , fresh = require('fresh');
/** * Static cache: * * Enables a memory cache layer on top of * the `static()` middleware, serving popular * static files. * * By default a maximum of 128 objects are * held in cache, with a max of 256k each, * totalling ~32mb. * * A Least-Recently-Used (LRU) cache algo * is implemented through the `Cache` object, * simply rotating cache objects as they are * hit. This means that increasingly popular * objects maintain their positions while * others get shoved out of the stack and * garbage collected. * * Benchmarks: * * static(): 2700 rps * node-static: 5300 rps * static() + staticCache(): 7500 rps * * Options: * * - `maxObjects` max cache objects [128] * - `maxLength` max cache object length 256kb * * @param {Object} options * @return {Function} * @api public */
module.exports = function staticCache(options){ var options = options || {} , cache = new Cache(options.maxObjects || 128) , maxlen = options.maxLength || 1024 * 256;
console.warn('connect.staticCache() is deprecated and will be removed in 3.0'); console.warn('use varnish or similar reverse proxy caches.');
return function staticCache(req, res, next){ var key = cacheKey(req) , ranges = req.headers.range , hasCookies = req.headers.cookie , hit = cache.get(key);
// cache static
// TODO: change from staticCache() -> cache()
// and make this work for any request
req.on('static', function(stream){ var headers = res._headers , cc = utils.parseCacheControl(headers['cache-control'] || '') , contentLength = headers['content-length'] , hit;
// dont cache set-cookie responses
if (headers['set-cookie']) return hasCookies = true;
// dont cache when cookies are present
if (hasCookies) return;
// ignore larger files
if (!contentLength || contentLength > maxlen) return;
// don't cache partial files
if (headers['content-range']) return;
// dont cache items we shouldn't be
// TODO: real support for must-revalidate / no-cache
if ( cc['no-cache'] || cc['no-store'] || cc['private'] || cc['must-revalidate']) return;
// if already in cache then validate
if (hit = cache.get(key)){ if (headers.etag == hit[0].etag) { hit[0].date = new Date; return; } else { cache.remove(key); } }
// validation notifiactions don't contain a steam
if (null == stream) return;
// add the cache object
var arr = [];
// store the chunks
stream.on('data', function(chunk){ arr.push(chunk); });
// flag it as complete
stream.on('end', function(){ var cacheEntry = cache.add(key); delete headers['x-cache']; // Clean up (TODO: others)
cacheEntry.push(200); cacheEntry.push(headers); cacheEntry.push.apply(cacheEntry, arr); }); });
if (req.method == 'GET' || req.method == 'HEAD') { if (ranges) { next(); } else if (!hasCookies && hit && !mustRevalidate(req, hit)) { res.setHeader('X-Cache', 'HIT'); respondFromCache(req, res, hit); } else { res.setHeader('X-Cache', 'MISS'); next(); } } else { next(); } } };
/** * Respond with the provided cached value. * TODO: Assume 200 code, that's iffy. * * @param {Object} req * @param {Object} res * @param {Object} cacheEntry * @return {String} * @api private */
function respondFromCache(req, res, cacheEntry) { var status = cacheEntry[0] , headers = utils.merge({}, cacheEntry[1]) , content = cacheEntry.slice(2);
headers.age = (new Date - new Date(headers.date)) / 1000 || 0;
switch (req.method) { case 'HEAD': res.writeHead(status, headers); res.end(); break; case 'GET': if (utils.conditionalGET(req) && fresh(req.headers, headers)) { headers['content-length'] = 0; res.writeHead(304, headers); res.end(); } else { res.writeHead(status, headers);
function write() { while (content.length) { if (false === res.write(content.shift())) { res.once('drain', write); return; } } res.end(); }
write(); } break; default: // This should never happen.
res.writeHead(500, ''); res.end(); } }
/** * Determine whether or not a cached value must be revalidated. * * @param {Object} req * @param {Object} cacheEntry * @return {String} * @api private */
function mustRevalidate(req, cacheEntry) { var cacheHeaders = cacheEntry[1] , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '') , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '') , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0;
if ( cacheCC['no-cache'] || cacheCC['must-revalidate'] || cacheCC['proxy-revalidate']) return true;
if (reqCC['no-cache']) return true;
if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge;
if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge;
return false; }
/** * The key to use in the cache. For now, this is the URL path and query. * * 'http://example.com?key=value' -> '/?key=value' * * @param {Object} req * @return {String} * @api private */
function cacheKey(req) { return utils.parseUrl(req).path; }
|