You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
175 lines
4.1 KiB
175 lines
4.1 KiB
|
|
/*! |
|
* 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; |
|
} |