staticCache.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231
  1. /*!
  2. * Connect - staticCache
  3. * Copyright(c) 2011 Sencha Inc.
  4. * MIT Licensed
  5. */
  6. /**
  7. * Module dependencies.
  8. */
  9. var utils = require('../utils')
  10. , Cache = require('../cache')
  11. , fresh = require('fresh');
  12. /**
  13. * Static cache:
  14. *
  15. * Enables a memory cache layer on top of
  16. * the `static()` middleware, serving popular
  17. * static files.
  18. *
  19. * By default a maximum of 128 objects are
  20. * held in cache, with a max of 256k each,
  21. * totalling ~32mb.
  22. *
  23. * A Least-Recently-Used (LRU) cache algo
  24. * is implemented through the `Cache` object,
  25. * simply rotating cache objects as they are
  26. * hit. This means that increasingly popular
  27. * objects maintain their positions while
  28. * others get shoved out of the stack and
  29. * garbage collected.
  30. *
  31. * Benchmarks:
  32. *
  33. * static(): 2700 rps
  34. * node-static: 5300 rps
  35. * static() + staticCache(): 7500 rps
  36. *
  37. * Options:
  38. *
  39. * - `maxObjects` max cache objects [128]
  40. * - `maxLength` max cache object length 256kb
  41. *
  42. * @param {Object} options
  43. * @return {Function}
  44. * @api public
  45. */
  46. module.exports = function staticCache(options){
  47. var options = options || {}
  48. , cache = new Cache(options.maxObjects || 128)
  49. , maxlen = options.maxLength || 1024 * 256;
  50. console.warn('connect.staticCache() is deprecated and will be removed in 3.0');
  51. console.warn('use varnish or similar reverse proxy caches.');
  52. return function staticCache(req, res, next){
  53. var key = cacheKey(req)
  54. , ranges = req.headers.range
  55. , hasCookies = req.headers.cookie
  56. , hit = cache.get(key);
  57. // cache static
  58. // TODO: change from staticCache() -> cache()
  59. // and make this work for any request
  60. req.on('static', function(stream){
  61. var headers = res._headers
  62. , cc = utils.parseCacheControl(headers['cache-control'] || '')
  63. , contentLength = headers['content-length']
  64. , hit;
  65. // dont cache set-cookie responses
  66. if (headers['set-cookie']) return hasCookies = true;
  67. // dont cache when cookies are present
  68. if (hasCookies) return;
  69. // ignore larger files
  70. if (!contentLength || contentLength > maxlen) return;
  71. // don't cache partial files
  72. if (headers['content-range']) return;
  73. // dont cache items we shouldn't be
  74. // TODO: real support for must-revalidate / no-cache
  75. if ( cc['no-cache']
  76. || cc['no-store']
  77. || cc['private']
  78. || cc['must-revalidate']) return;
  79. // if already in cache then validate
  80. if (hit = cache.get(key)){
  81. if (headers.etag == hit[0].etag) {
  82. hit[0].date = new Date;
  83. return;
  84. } else {
  85. cache.remove(key);
  86. }
  87. }
  88. // validation notifiactions don't contain a steam
  89. if (null == stream) return;
  90. // add the cache object
  91. var arr = [];
  92. // store the chunks
  93. stream.on('data', function(chunk){
  94. arr.push(chunk);
  95. });
  96. // flag it as complete
  97. stream.on('end', function(){
  98. var cacheEntry = cache.add(key);
  99. delete headers['x-cache']; // Clean up (TODO: others)
  100. cacheEntry.push(200);
  101. cacheEntry.push(headers);
  102. cacheEntry.push.apply(cacheEntry, arr);
  103. });
  104. });
  105. if (req.method == 'GET' || req.method == 'HEAD') {
  106. if (ranges) {
  107. next();
  108. } else if (!hasCookies && hit && !mustRevalidate(req, hit)) {
  109. res.setHeader('X-Cache', 'HIT');
  110. respondFromCache(req, res, hit);
  111. } else {
  112. res.setHeader('X-Cache', 'MISS');
  113. next();
  114. }
  115. } else {
  116. next();
  117. }
  118. }
  119. };
  120. /**
  121. * Respond with the provided cached value.
  122. * TODO: Assume 200 code, that's iffy.
  123. *
  124. * @param {Object} req
  125. * @param {Object} res
  126. * @param {Object} cacheEntry
  127. * @return {String}
  128. * @api private
  129. */
  130. function respondFromCache(req, res, cacheEntry) {
  131. var status = cacheEntry[0]
  132. , headers = utils.merge({}, cacheEntry[1])
  133. , content = cacheEntry.slice(2);
  134. headers.age = (new Date - new Date(headers.date)) / 1000 || 0;
  135. switch (req.method) {
  136. case 'HEAD':
  137. res.writeHead(status, headers);
  138. res.end();
  139. break;
  140. case 'GET':
  141. if (utils.conditionalGET(req) && fresh(req.headers, headers)) {
  142. headers['content-length'] = 0;
  143. res.writeHead(304, headers);
  144. res.end();
  145. } else {
  146. res.writeHead(status, headers);
  147. function write() {
  148. while (content.length) {
  149. if (false === res.write(content.shift())) {
  150. res.once('drain', write);
  151. return;
  152. }
  153. }
  154. res.end();
  155. }
  156. write();
  157. }
  158. break;
  159. default:
  160. // This should never happen.
  161. res.writeHead(500, '');
  162. res.end();
  163. }
  164. }
  165. /**
  166. * Determine whether or not a cached value must be revalidated.
  167. *
  168. * @param {Object} req
  169. * @param {Object} cacheEntry
  170. * @return {String}
  171. * @api private
  172. */
  173. function mustRevalidate(req, cacheEntry) {
  174. var cacheHeaders = cacheEntry[1]
  175. , reqCC = utils.parseCacheControl(req.headers['cache-control'] || '')
  176. , cacheCC = utils.parseCacheControl(cacheHeaders['cache-control'] || '')
  177. , cacheAge = (new Date - new Date(cacheHeaders.date)) / 1000 || 0;
  178. if ( cacheCC['no-cache']
  179. || cacheCC['must-revalidate']
  180. || cacheCC['proxy-revalidate']) return true;
  181. if (reqCC['no-cache']) return true;
  182. if (null != reqCC['max-age']) return reqCC['max-age'] < cacheAge;
  183. if (null != cacheCC['max-age']) return cacheCC['max-age'] < cacheAge;
  184. return false;
  185. }
  186. /**
  187. * The key to use in the cache. For now, this is the URL path and query.
  188. *
  189. * 'http://example.com?key=value' -> '/?key=value'
  190. *
  191. * @param {Object} req
  192. * @return {String}
  193. * @api private
  194. */
  195. function cacheKey(req) {
  196. return utils.parseUrl(req).path;
  197. }