| |
| /*! |
| * 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; |
| } |