175 lines
4.1 KiB
JavaScript
175 lines
4.1 KiB
JavaScript
|
|
||
|
/*!
|
||
|
* Connect - staticCache
|
||
|
* Copyright(c) 2011 Sencha Inc.
|
||
|
* MIT Licensed
|
||
|
*/
|
||
|
|
||
|
/**
|
||
|
* Module dependencies.
|
||
|
*/
|
||
|
|
||
|
var http = require('http')
|
||
|
, utils = require('../utils')
|
||
|
, Cache = require('../cache')
|
||
|
, url = require('url')
|
||
|
, fs = require('fs');
|
||
|
|
||
|
/**
|
||
|
* 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 {Type} name
|
||
|
* @return {Type}
|
||
|
* @api public
|
||
|
*/
|
||
|
|
||
|
module.exports = function staticCache(options){
|
||
|
var options = options || {}
|
||
|
, cache = new Cache(options.maxObjects || 128)
|
||
|
, maxlen = options.maxLength || 1024 * 256;
|
||
|
|
||
|
return function staticCache(req, res, next){
|
||
|
var path = url.parse(req.url).pathname
|
||
|
, ranges = req.headers.range
|
||
|
, hit = cache.get(path)
|
||
|
, hitCC
|
||
|
, uaCC
|
||
|
, header
|
||
|
, age;
|
||
|
|
||
|
// cache static
|
||
|
req.on('static', function(stream){
|
||
|
var headers = res._headers
|
||
|
, cc = utils.parseCacheControl(headers['cache-control'] || '')
|
||
|
, contentLength = headers['content-length']
|
||
|
, hit;
|
||
|
|
||
|
// ignore larger files
|
||
|
if (!contentLength || contentLength > maxlen) return;
|
||
|
|
||
|
// dont cache items we shouldn't be
|
||
|
if ( cc['no-cache']
|
||
|
|| cc['no-store']
|
||
|
|| cc['private']
|
||
|
|| cc['must-revalidate']) return;
|
||
|
|
||
|
// if already in cache then validate
|
||
|
if (hit = cache.get(path)){
|
||
|
if (headers.etag == hit[0].etag) {
|
||
|
hit[0].date = new Date;
|
||
|
return;
|
||
|
} else {
|
||
|
cache.remove(path);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// validation notifiactions don't contain a steam
|
||
|
if (null == stream) return;
|
||
|
|
||
|
// add the cache object
|
||
|
var arr = cache.add(path);
|
||
|
arr.push(headers);
|
||
|
|
||
|
// store the chunks
|
||
|
stream.on('data', function(chunk){
|
||
|
arr.push(chunk);
|
||
|
});
|
||
|
|
||
|
// flag it as complete
|
||
|
stream.on('end', function(){
|
||
|
arr.complete = true;
|
||
|
});
|
||
|
});
|
||
|
|
||
|
// cache hit, doesnt support range requests
|
||
|
if (hit && hit.complete && !ranges) {
|
||
|
header = utils.merge({}, hit[0]);
|
||
|
header.Age = age = (new Date - new Date(header.date)) / 1000 | 0;
|
||
|
header.date = new Date().toUTCString();
|
||
|
|
||
|
// parse cache-controls
|
||
|
hitCC = utils.parseCacheControl(header['cache-control'] || '');
|
||
|
uaCC = utils.parseCacheControl(req.headers['cache-control'] || '');
|
||
|
|
||
|
// check if we must revalidate(bypass)
|
||
|
if (hitCC['no-cache'] || uaCC['no-cache']) return next();
|
||
|
|
||
|
// check freshness of entity
|
||
|
if (isStale(hitCC, age) || isStale(uaCC, age)) return next();
|
||
|
|
||
|
// conditional GET support
|
||
|
if (utils.conditionalGET(req)) {
|
||
|
if (!utils.modified(req, res, header)) {
|
||
|
header['content-length'] = 0;
|
||
|
res.writeHead(304, header);
|
||
|
return res.end();
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// HEAD support
|
||
|
if ('HEAD' == req.method) {
|
||
|
header['content-length'] = 0;
|
||
|
res.writeHead(200, header);
|
||
|
return res.end();
|
||
|
}
|
||
|
|
||
|
// respond with cache
|
||
|
res.writeHead(200, header);
|
||
|
|
||
|
// backpressure
|
||
|
function write(i) {
|
||
|
var buf = hit[i];
|
||
|
if (!buf) return res.end();
|
||
|
if (false === res.write(buf)) {
|
||
|
res.once('drain', function(){
|
||
|
write(++i);
|
||
|
});
|
||
|
} else {
|
||
|
write(++i);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
return write(1);
|
||
|
}
|
||
|
|
||
|
next();
|
||
|
}
|
||
|
};
|
||
|
|
||
|
/**
|
||
|
* Check if cache item is stale
|
||
|
*
|
||
|
* @param {Object} cc
|
||
|
* @param {Number} age
|
||
|
* @return {Boolean}
|
||
|
* @api private
|
||
|
*/
|
||
|
|
||
|
function isStale(cc, age) {
|
||
|
return cc['max-age'] && cc['max-age'] <= age;
|
||
|
}
|