3 * Connect - staticCache
4 * Copyright(c) 2011 Sencha Inc.
12 var deprecate = require('depd')('connect');
13 var utils = require('../utils')
14 , parseurl = require('parseurl')
15 , Cache = require('../cache')
16 , fresh = require('fresh');
17 var merge = require('utils-merge');
22 * Status: Deprecated. This middleware will be removed in
23 * Connect 3.0. You may be interested in:
25 * - [st](https://github.com/isaacs/st)
27 * Enables a memory cache layer on top of
28 * the `static()` middleware, serving popular
31 * By default a maximum of 128 objects are
32 * held in cache, with a max of 256k each,
35 * A Least-Recently-Used (LRU) cache algo
36 * is implemented through the `Cache` object,
37 * simply rotating cache objects as they are
38 * hit. This means that increasingly popular
39 * objects maintain their positions while
40 * others get shoved out of the stack and
46 * node-static: 5300 rps
47 * static() + staticCache(): 7500 rps
51 * - `maxObjects` max cache objects [128]
52 * - `maxLength` max cache object length 256kb
54 * @param {Object} options
59 module.exports = function staticCache(options){
60 var options = options || {}
61 , cache = new Cache(options.maxObjects || 128)
62 , maxlen = options.maxLength || 1024 * 256;
64 return function staticCache(req, res, next){
65 var key = cacheKey(req)
66 , ranges = req.headers.range
67 , hasCookies = req.headers.cookie
68 , hit = cache.get(key);
71 // TODO: change from staticCache() -> cache()
72 // and make this work for any request
73 req.on('static', function(stream){
74 var headers = res._headers
75 , cc = utils.parseCacheControl(headers['cache-control'] || '')
76 , contentLength = headers['content-length']
79 // dont cache set-cookie responses
80 if (headers['set-cookie']) return hasCookies = true;
82 // dont cache when cookies are present
83 if (hasCookies) return;
85 // ignore larger files
86 if (!contentLength || contentLength > maxlen) return;
88 // don't cache partial files
89 if (headers['content-range']) return;
91 // dont cache items we shouldn't be
92 // TODO: real support for must-revalidate / no-cache
96 || cc['must-revalidate']) return;
98 // if already in cache then validate
99 if (hit = cache.get(key)){
100 if (headers.etag == hit[0].etag) {
101 hit[0].date = new Date;
108 // validation notifiactions don't contain a steam
109 if (null == stream) return;
111 // add the cache object
115 stream.on('data', function(chunk){
119 // flag it as complete
120 stream.on('end', function(){
121 var cacheEntry = cache.add(key);
122 delete headers['x-cache']; // Clean up (TODO: others)
123 cacheEntry.push(200);
124 cacheEntry.push(headers);
125 cacheEntry.push.apply(cacheEntry, arr);
129 if (req.method == 'GET' || req.method == 'HEAD') {
132 } else if (!hasCookies && hit && !mustRevalidate(req, hit)) {
133 res.setHeader('X-Cache', 'HIT');
134 respondFromCache(req, res, hit);
136 res.setHeader('X-Cache', 'MISS');
145 module.exports = deprecate.function(module.exports,
146 'staticCache: use varnish or similar reverse proxy caches');
149 * Respond with the provided cached value.
150 * TODO: Assume 200 code, that's iffy.
152 * @param {Object} req
153 * @param {Object} res
154 * @param {Object} cacheEntry
159 function respondFromCache(req, res, cacheEntry) {
160 var status = cacheEntry[0]
161 , headers = merge({}, cacheEntry[1])
162 , content = cacheEntry.slice(2);
164 headers.age = (new Date - new Date(headers.date)) / 1000 || 0;
166 switch (req.method) {
168 res.writeHead(status, headers);
172 if (fresh(req.headers, headers)) {
173 headers['content-length'] = 0;
174 res.writeHead(304, headers);
177 res.writeHead(status, headers);
180 while (content.length) {
181 if (false === res.write(content.shift())) {
182 res.once('drain', write);
193 // This should never happen.
194 res.writeHead(500, '');
200 * Determine whether or not a cached value must be revalidated.
202 * @param {Object} req
203 * @param {Object} cacheEntry
208 function mustRevalidate(req, cacheEntry) {
209 var cacheHeaders = cacheEntry[1]
210 , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '')
211 , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '')
212 , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0;
214 if ( cacheCC['no-cache']
215 || cacheCC['must-revalidate']
216 || cacheCC['proxy-revalidate']) return true;
218 if (reqCC['no-cache']) return true;
220 if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge;
222 if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge;
228 * The key to use in the cache. For now, this is the URL path and query.
230 * 'http://example.com?key=value' -> '/?key=value'
232 * @param {Object} req
237 function cacheKey(req) {
238 return parseurl(req).path;